From ffba1d4f7a8a626c7bdcb875531e1d587fd7266e Mon Sep 17 00:00:00 2001 From: asur4s Date: Thu, 15 Dec 2022 03:51:50 -0800 Subject: [PATCH 001/734] refactor: release key of server --- src/server/input_service.rs | 168 +++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 61 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 021d85732..7a1ae6592 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -69,6 +69,7 @@ struct Input { y: i32, } +const KEY_RDEV_START: u64 = 999; const KEY_CHAR_START: u64 = 9999; #[derive(Clone, Default)] @@ -339,7 +340,7 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) { pub fn fix_key_down_timeout_loop() { std::thread::spawn(move || loop { - std::thread::sleep(std::time::Duration::from_millis(1_000)); + std::thread::sleep(std::time::Duration::from_millis(10_000)); fix_key_down_timeout(false); }); if let Err(err) = ctrlc::set_handler(move || { @@ -360,38 +361,61 @@ pub fn fix_key_down_timeout_at_exit() { } #[inline] -fn get_layout(key: u32) -> Key { - Key::Layout(std::char::from_u32(key).unwrap_or('\0')) +fn record_key_is_control_key(record_key: u64) -> bool { + record_key < KEY_CHAR_START +} + +#[inline] +fn record_key_is_chr(record_key: u64) -> bool { + KEY_RDEV_START <= record_key && record_key < KEY_CHAR_START +} + +#[inline] +fn record_key_is_rdev_layout(record_key: u64) -> bool { + KEY_CHAR_START <= record_key +} + +#[inline] +fn record_key_to_key(record_key: u64) -> Option { + if record_key_is_control_key(record_key) { + control_key_value_to_key(record_key as _) + } else if record_key_is_chr(record_key) { + let chr: u32 = (record_key - KEY_CHAR_START) as _; + Some(char_value_to_key(chr)) + } else { + None + } +} + +#[inline] +fn release_record_key(record_key: u64) { + let func = move || { + if record_key_is_rdev_layout(record_key) { + rdev_key_down_or_up(RdevKey::Unknown((record_key - KEY_RDEV_START) as _), false); + } else if let Some(key) = record_key_to_key(record_key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + }; + + #[cfg(target_os = "macos")] + QUEUE.exec_async(func); + #[cfg(not(target_os = "macos"))] + func(); } fn fix_key_down_timeout(force: bool) { - if KEYS_DOWN.lock().unwrap().is_empty() { + let key_down = KEYS_DOWN.lock().unwrap(); + if key_down.is_empty() { return; } - let cloned = (*KEYS_DOWN.lock().unwrap()).clone(); - for (key, value) in cloned.into_iter() { - if force || value.elapsed().as_millis() >= 360_000 { - KEYS_DOWN.lock().unwrap().remove(&key); - let key = if key < KEY_CHAR_START { - if let Some(key) = KEY_MAP.get(&(key as _)) { - Some(*key) - } else { - None - } - } else { - Some(get_layout((key - KEY_CHAR_START) as _)) - }; - if let Some(key) = key { - let func = move || { - let mut en = ENIGO.lock().unwrap(); - en.key_up(key); - log::debug!("Fixed {:?} timeout", key); - }; - #[cfg(target_os = "macos")] - QUEUE.exec_async(func); - #[cfg(not(target_os = "macos"))] - func(); - } + let cloned = (*key_down).clone(); + drop(key_down); + + for (record_key, time) in cloned.into_iter() { + if force || time.elapsed().as_millis() >= 360_000 { + record_pressed_key(record_key, false); + release_record_key(record_key); } } } @@ -685,8 +709,14 @@ fn is_modifier_in_key_event(control_key: ControlKey, key_event: &KeyEvent) -> bo .is_some() } -fn control_key_to_key(control_key: &EnumOrUnknown) -> Option<&Key> { - KEY_MAP.get(&control_key.value()) +#[inline] +fn control_key_value_to_key(value: i32) -> Option { + KEY_MAP.get(&value).and_then(|k| Some(*k)) +} + +#[inline] +fn char_value_to_key(value: u32) -> Key { + Key::Layout(std::char::from_u32(value).unwrap_or('\0')) } fn is_not_same_status(client_locking: bool, remote_locking: bool) -> bool { @@ -772,6 +802,8 @@ fn sync_numlock_capslock_status(key_event: &KeyEvent) { fn map_keyboard_mode(evt: &KeyEvent) { // map mode(1): Send keycode according to the peer platform. + record_pressed_key(evt.chr() as u64 + KEY_CHAR_START, evt.down); + #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -818,7 +850,7 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { fix_modifiers(&key_event.modifiers[..], en, ck_value); } -fn is_altgr_pressed(en: &mut Enigo) -> bool { +fn is_altgr_pressed() -> bool { KEYS_DOWN .lock() .unwrap() @@ -828,10 +860,10 @@ fn is_altgr_pressed(en: &mut Enigo) -> bool { fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { for ref ck in key_event.modifiers.iter() { - if let Some(key) = control_key_to_key(ck) { - if !is_pressed(key, en) { + if let Some(key) = control_key_value_to_key(ck.value()) { + if !is_pressed(&key, en) { #[cfg(target_os = "linux")] - if key == &Key::Alt && is_altgr_pressed(en) { + if key == Key::Alt && is_altgr_pressed() { continue; } en.key_down(key.clone()).ok(); @@ -855,44 +887,25 @@ fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec, down: bool) { - let mut key_down = KEYS_DOWN.lock().unwrap(); - - if ck.value() == ControlKey::CtrlAltDel.value() { - // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. - std::thread::spawn(|| { - allow_err!(send_sas()); - }); - } else if ck.value() == ControlKey::LockScreen.value() { - lock_screen_2(); - } else if let Some(key) = control_key_to_key(ck) { + if let Some(key) = control_key_value_to_key(ck.value()) { if down { - en.key_down(key.clone()).ok(); - key_down.insert(ck.value() as _, Instant::now()); + en.key_down(key).ok(); } else { - en.key_up(key.clone()); - key_down.remove(&(ck.value() as _)); + en.key_up(key); } } } -#[inline] -fn chr_to_record_chr(chr: u32) -> u64 { - chr as u64 + KEY_CHAR_START -} - #[inline] fn need_to_uppercase(en: &mut Enigo) -> bool { get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) } fn process_chr(en: &mut Enigo, chr: u32, down: bool) { - let mut key_down = KEYS_DOWN.lock().unwrap(); - let key = get_layout(chr); - let record_chr = chr_to_record_chr(chr); + let key = char_value_to_key(chr); if down { if en.key_down(key).is_ok() { - key_down.insert(record_chr, Instant::now()); } else { if let Ok(chr) = char::try_from(chr) { let mut s = chr.to_string(); @@ -902,10 +915,8 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) { en.key_sequence(&s); }; } - key_down.insert(record_chr, Instant::now()); } else { en.key_up(key); - key_down.remove(&record_chr); } } @@ -925,6 +936,30 @@ fn release_keys(en: &mut Enigo, to_release: &Vec) { } } +fn record_pressed_key(record_key: u64, down: bool) { + let mut key_down = KEYS_DOWN.lock().unwrap(); + if down { + key_down.insert(record_key, Instant::now()); + } else { + key_down.remove(&record_key); + } +} + +fn is_function_key(ck: &EnumOrUnknown) -> bool { + let mut res = false; + if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + res = true; + } else if ck.value() == ControlKey::LockScreen.value() { + lock_screen_2(); + res = true; + } + return res; +} + fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -936,8 +971,19 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { let down = evt.down; match evt.union { - Some(key_event::Union::ControlKey(ck)) => process_control_key(&mut en, &ck, down), - Some(key_event::Union::Chr(chr)) => process_chr(&mut en, chr, down), + Some(key_event::Union::ControlKey(ck)) => { + if is_function_key(&ck) { + return; + } + let record_key = ck.value() as u64; + record_pressed_key(record_key, down); + process_control_key(&mut en, &ck, down) + } + Some(key_event::Union::Chr(chr)) => { + let record_key = chr as u64 + KEY_CHAR_START; + record_pressed_key(record_key, down); + process_chr(&mut en, chr, down) + } Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), _ => {} From 4837d84209ee0de8bfd2ffa8938ec3ae029e3ab9 Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 20 Dec 2022 01:09:35 -0800 Subject: [PATCH 002/734] opt: enum KeyboardMode --- libs/hbb_common/src/keyboard.rs | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 libs/hbb_common/src/keyboard.rs diff --git a/libs/hbb_common/src/keyboard.rs b/libs/hbb_common/src/keyboard.rs new file mode 100644 index 000000000..10979f520 --- /dev/null +++ b/libs/hbb_common/src/keyboard.rs @@ -0,0 +1,39 @@ +use std::{fmt, slice::Iter, str::FromStr}; + +use crate::protos::message::KeyboardMode; + +impl fmt::Display for KeyboardMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeyboardMode::Legacy => write!(f, "legacy"), + KeyboardMode::Map => write!(f, "map"), + KeyboardMode::Translate => write!(f, "translate"), + KeyboardMode::Auto => write!(f, "auto"), + } + } +} + +impl FromStr for KeyboardMode { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "legacy" => Ok(KeyboardMode::Legacy), + "map" => Ok(KeyboardMode::Map), + "translate" => Ok(KeyboardMode::Translate), + "auto" => Ok(KeyboardMode::Auto), + _ => Err(()), + } + } +} + +impl KeyboardMode { + pub fn iter() -> Iter<'static, KeyboardMode> { + static KEYBOARD_MODES: [KeyboardMode; 4] = [ + KeyboardMode::Legacy, + KeyboardMode::Map, + KeyboardMode::Translate, + KeyboardMode::Auto, + ]; + KEYBOARD_MODES.iter() + } +} From c267fc9d9bfa4f6ff7a155fd9c3abd4ae3b67c0e Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 20 Dec 2022 01:11:52 -0800 Subject: [PATCH 003/734] refactor: set default keyboard mode --- src/client.rs | 16 +++++++++------- src/common.rs | 9 +++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/client.rs b/src/client.rs index 03bbf5918..1bb2ff861 100644 --- a/src/client.rs +++ b/src/client.rs @@ -23,7 +23,7 @@ use hbb_common::{ Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT, }, - log, + get_version_number, log, message_proto::{option_message::BoolOption, *}, protobuf::Message as _, rand, @@ -47,7 +47,10 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}; +use crate::{ + common::is_keyboard_mode_supported, + server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, +}; 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); @@ -1410,12 +1413,11 @@ impl LoginConfigHandler { log::debug!("remove password of {}", self.id); } } - if config.keyboard_mode == "" { - if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") - { - config.keyboard_mode = "legacy".to_string(); + if config.keyboard_mode.is_empty() { + if is_keyboard_mode_supported(&KeyboardMode::Map, get_version_number(&pi.version)) { + config.keyboard_mode = KeyboardMode::Map.to_string(); } else { - config.keyboard_mode = "map".to_string(); + config.keyboard_mode = KeyboardMode::Legacy.to_string(); } } self.conn_id = pi.conn_id; diff --git a/src/common.rs b/src/common.rs index 9023780f4..0f3794261 100644 --- a/src/common.rs +++ b/src/common.rs @@ -689,6 +689,15 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess msg_out } +pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: i64) -> bool { + match keyboard_mode { + KeyboardMode::Legacy => true, + KeyboardMode::Map => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Translate => true, + KeyboardMode::Auto => true, + } +} + #[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { pub static ref IS_X11: Mutex = Mutex::new(false); From 85620b73a74780b510f2fbbca624baa9726303e3 Mon Sep 17 00:00:00 2001 From: asur4s Date: Wed, 21 Dec 2022 00:03:15 -0800 Subject: [PATCH 004/734] opt: get supported keyboard modes --- libs/hbb_common/src/lib.rs | 1 + src/common.rs | 4 ++-- src/ui_session_interface.rs | 14 +++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index ae564685f..4245b1d73 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -40,6 +40,7 @@ pub use tokio_socks::TargetAddr; pub mod password_security; pub use chrono; pub use directories_next; +pub mod keyboard; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/src/common.rs b/src/common.rs index 0f3794261..02b4a0c10 100644 --- a/src/common.rs +++ b/src/common.rs @@ -693,8 +693,8 @@ pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: match keyboard_mode { KeyboardMode::Legacy => true, KeyboardMode::Map => version_number >= hbb_common::get_version_number("1.2.0"), - KeyboardMode::Translate => true, - KeyboardMode::Auto => true, + KeyboardMode::Translate => false, + KeyboardMode::Auto => false, } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 0cf2f2e2d..e7ac620ee 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -4,7 +4,7 @@ use crate::client::{ load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -use crate::common::GrabState; +use crate::common::{is_keyboard_mode_supported, GrabState}; use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -48,6 +48,10 @@ impl Session { self.lc.read().unwrap().custom_image_quality.clone() } + pub fn get_peer_version(&self) -> i64 { + self.lc.read().unwrap().version.clone() + } + pub fn get_keyboard_mode(&self) -> String { self.lc.read().unwrap().keyboard_mode.clone() } @@ -198,6 +202,14 @@ impl Session { crate::platform::is_xfce() } + pub fn get_supported_keyboard_modes(&self) -> Vec { + let version = self.get_peer_version(); + KeyboardMode::iter() + .filter(|&mode| is_keyboard_mode_supported(mode, version)) + .map(|&mode| mode) + .collect::>() + } + pub fn remove_port_forward(&self, port: i32) { let mut config = self.load_config(); config.port_forwards = config From a3769ca8e937116faaaaf1a4a64235e26fd3b332 Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 26 Dec 2022 02:30:25 -0800 Subject: [PATCH 005/734] opt: map mode hide when unsupported --- .../lib/desktop/widgets/remote_menubar.dart | 23 +++++++++++++++---- src/flutter_ffi.rs | 18 +++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c1a7bdce2..fc35fb47e 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1185,10 +1185,25 @@ class _RemoteMenubarState extends State { final keyboardMenu = [ MenuEntryRadios( text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), - MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), - ], + optionsGetter: () { + List list = []; + List modes = ["legacy"]; + + if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) { + modes.add("map"); + } + + for (String mode in modes) { + if (mode == "legacy") { + list.add(MenuEntryRadioOption( + text: translate('Legacy mode'), value: 'legacy')); + } else if (mode == "map") { + list.add(MenuEntryRadioOption( + text: translate('Map mode'), value: 'map')); + } + } + return list; + }, curOptionGetter: () async { return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; }, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ddfaad06d..61b01f50c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -7,11 +7,14 @@ use std::{ use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; +use crate::common::is_keyboard_mode_supported; +use hbb_common::message_proto::KeyboardMode; use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, }; +use std::str::FromStr; // use crate::hbbs_http::account::AuthResult; @@ -245,6 +248,21 @@ pub fn session_get_custom_image_quality(id: String) -> Option> { } } +pub fn session_is_keyboard_mode_supported(id: String, mode: String) -> SyncReturn { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { + SyncReturn(is_keyboard_mode_supported( + &mode, + session.get_peer_version(), + )) + } else { + SyncReturn(false) + } + } else { + SyncReturn(false) + } +} + pub fn session_set_custom_image_quality(id: String, value: i32) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_custom_image_quality(value); From 95844e60cfce5c92351b4a2d7ea3ab381e3ce537 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 00:40:41 +0800 Subject: [PATCH 006/734] win filter scancodes that is greater than 255 Signed-off-by: fufesou --- src/keyboard.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 9fa53757f..f29eb27bc 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -65,7 +65,7 @@ pub mod client { #[cfg(not(feature = "cli"))] if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { return handler.get_keyboard_mode(); - } + } "legacy".to_string() } @@ -372,7 +372,7 @@ pub fn get_peer_platform() -> String { #[cfg(not(feature = "cli"))] if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { return handler.peer_platform(); - } + } "Windows".to_string() } @@ -615,7 +615,13 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option event.scan_code, + "windows" => { + // https://github.com/rustdesk/rustdesk/issues/1371 + if event.scan_code > 255 { + return None; + } + event.scan_code + } "macos" => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::win_scancode_to_macos_iso_code(event.scan_code)? From b75453b08f5ff731884f20f07a4b952cce5d6811 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:20:52 -0500 Subject: [PATCH 007/734] spelling: a workaround Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/cm.tis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 716f2c6dd..a1d623322 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -31,7 +31,7 @@ class Body: Reactor.Component var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; - // below size:* is work around for Linux, it already set in css, but not work, shit sciter + // below size:* is a workaround for Linux, it already set in css, but not work, shit sciter return
From 8351d331b4d75705926da73c95d0abd322c0e133 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:17 -0500 Subject: [PATCH 008/734] spelling: acceleration Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/remote.tis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 63df0cb09..5c828689d 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -120,7 +120,7 @@ function resetWheel() { var INERTIA_ACCELERATION = 30; -// not good, precision not enough to simulate accelation effect, +// not good, precision not enough to simulate acceleration effect, // seems have to use pixel based rather line based delta function accWheel(v, is_x) { if (wheeling) return; From 2b93de18ce8d559eb6d30afb7d46461e72473ab2 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:19 -0500 Subject: [PATCH 009/734] spelling: activate Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 635c8b661..440c0b0b0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1662,7 +1662,7 @@ pub fn send_mouse( interface.send(Data::Message(msg_out)); } -/// Avtivate OS by sending mouse movement. +/// Activate OS by sending mouse movement. /// /// # Arguments /// @@ -1690,7 +1690,7 @@ fn activate_os(interface: &impl Interface) { /// # Arguments /// /// * `p` - The password. -/// * `avtivate` - Whether to activate OS. +/// * `activate` - Whether to activate OS. /// * `interface` - The interface for sending data. pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { std::thread::spawn(move || { @@ -1703,7 +1703,7 @@ pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { /// # Arguments /// /// * `p` - The password. -/// * `avtivate` - Whether to activate OS. +/// * `activate` - Whether to activate OS. /// * `interface` - The interface for sending data. fn _input_os_password(p: String, activate: bool, interface: impl Interface) { if activate { From b4bb5bfecfc81c85568f9b438f56908e5cc82a5d Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:31:17 -0500 Subject: [PATCH 010/734] spelling: active Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/input_service.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 41ce8fd9e..814fea110 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -487,7 +487,7 @@ fn active_mouse_(conn: i32) -> bool { return false; } - let in_actived_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE }; + let in_active_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE }; // Check if input is in valid range match crate::get_cursor_pos() { @@ -496,7 +496,7 @@ fn active_mouse_(conn: i32) -> bool { let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); (lock.x, lock.y) }; - let mut can_active = in_actived_dist(last_in_x, x) && in_actived_dist(last_in_y, y); + let mut can_active = in_active_dist(last_in_x, x) && in_active_dist(last_in_y, y); // The cursor may not have been moved to last input position if system is busy now. // While this is not a common case, we check it again after some time later. if !can_active { @@ -505,7 +505,7 @@ fn active_mouse_(conn: i32) -> bool { std::thread::sleep(std::time::Duration::from_micros(10)); // Sleep here can also somehow suppress delay accumulation. if let Some((x2, y2)) = crate::get_cursor_pos() { - can_active = in_actived_dist(last_in_x, x2) && in_actived_dist(last_in_y, y2); + can_active = in_active_dist(last_in_x, x2) && in_active_dist(last_in_y, y2); } } if !can_active { From cbfcc3657fef0e40920331dcf544070d0051a5e9 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:18 -0500 Subject: [PATCH 011/734] spelling: agreement Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/install.tis | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/install.tis b/src/ui/install.tis index 39301fd02..3a7920bcf 100644 --- a/src/ui/install.tis +++ b/src/ui/install.tis @@ -13,7 +13,7 @@ class Install: Reactor.Component {
{translate('Create start menu shortcuts')}
{translate('Create desktop icon')}
-
{translate('End-user license agreement')}
+
{translate('End-user license agreement')}
{translate('agreement_tip')}
@@ -46,7 +46,7 @@ class Install: Reactor.Component { } } - event click $(#aggrement) { + event click $(#agreement) { view.open_url("http://rustdesk.com/privacy"); } From 2929d0f6a5ed0f83e6ae0bdc9c2dbe727f106bb7 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:17 -0500 Subject: [PATCH 012/734] spelling: android Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- fastlane/metadata/android/en-US/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 1f35ef92d..f78b3a20b 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -2,7 +2,7 @@ An open-source remote desktop application, the open source TeamViewer alternativ Source code: https://github.com/rustdesk/rustdesk Doc: https://rustdesk.com/docs/en/manual/mobile/ -In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Addroid remote control. +In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Android remote control. In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. From 49c1b3a2df877c3798de827b9d40331e5b682926 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:18 -0500 Subject: [PATCH 013/734] spelling: another Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/lib/models/input_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 0137b784e..63f86078c 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -197,7 +197,7 @@ class InputModel { // Check update event type and set buttons to be sent. int buttons = _lastButtons; if (type == _kMouseEventMove) { - // flutter may emit move event if one button is pressed and anoter button + // flutter may emit move event if one button is pressed and another button // is pressing or releasing. if (evt.buttons != _lastButtons) { // For simplicity From f45fdaa46fc7a397ae8e45a211a867cd324de85d Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:46:31 -0500 Subject: [PATCH 014/734] spelling: appveyor Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/appveyor.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/enigo/appveyor.yml b/libs/enigo/appveyor.yml index af3142ad9..5ad7bc249 100644 --- a/libs/enigo/appveyor.yml +++ b/libs/enigo/appveyor.yml @@ -1,9 +1,9 @@ -# Appveyor configuration template for Rust using rustup for Rust installation +# AppVeyor configuration template for Rust using rustup for Rust installation # https://github.com/starkat99/appveyor-rust ## Operating System (VM environment) ## -# Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets. +# Rust needs at least Visual Studio 2013 AppVeyor OS for MSVC targets. os: Visual Studio 2015 ## Build Matrix ## @@ -83,7 +83,7 @@ environment: ### Allowed failures ### -# See Appveyor documentation for specific details. In short, place any channel or targets you wish +# See AppVeyor documentation for specific details. In short, place any channel or targets you wish # to allow build failures on (usually nightly at least is a wise choice). This will prevent a build # or test failure in the matching channels/targets from failing the entire build. matrix: @@ -95,7 +95,7 @@ matrix: ## Install Script ## -# This is the most important part of the Appveyor configuration. This installs the version of Rust +# This is the most important part of the AppVeyor configuration. This installs the version of Rust # specified by the 'channel' and 'target' environment variables from the build matrix. This uses # rustup to install Rust. # @@ -110,7 +110,7 @@ install: ## Build Script ## -# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents +# 'cargo test' takes care of building for us, so disable AppVeyor's build stage. This prevents # the "directory does not contain a project or solution file" error. build: false From 185ff9e91e6c9aaa94e26c09f2e0e20e2639d580 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:19 -0500 Subject: [PATCH 015/734] spelling: available Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/scrap/src/common/hwcodec.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index c77da3f8f..d92ed2a7d 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -293,8 +293,8 @@ pub fn check_config() { quality: DEFAULT_HW_QUALITY, rc: DEFAULT_RC, }; - let encoders = CodecInfo::score(Encoder::avaliable_encoders(ctx)); - let decoders = CodecInfo::score(Decoder::avaliable_decoders()); + let encoders = CodecInfo::score(Encoder::available_encoders(ctx)); + let decoders = CodecInfo::score(Decoder::available_decoders()); if let Ok(old_encoders) = get_config(CFG_KEY_ENCODER) { if let Ok(old_decoders) = get_config(CFG_KEY_DECODER) { From c40ae690e320044de543c049c06735ac7d404bec Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:20 -0500 Subject: [PATCH 016/734] spelling: capture Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/video_service.rs | 32 ++++++++++++++++---------------- src/server/wayland.rs | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index b986c785c..618b003e9 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -309,9 +309,9 @@ pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { } #[cfg(windows)] -fn check_uac_switch(privacy_mode_id: i32, captuerer_privacy_mode_id: i32) -> ResultType<()> { - if captuerer_privacy_mode_id != 0 { - if privacy_mode_id != captuerer_privacy_mode_id { +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()? { bail!("consent.exe is running"); } @@ -330,7 +330,7 @@ pub(super) struct CapturerInfo { pub ndisplay: usize, pub current: usize, pub privacy_mode_id: i32, - pub _captuerer_privacy_mode_id: i32, + pub _capturer_privacy_mode_id: i32, pub capturer: Box, } @@ -371,29 +371,29 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType ResultType<()> { while sp.ok() { #[cfg(windows)] - check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?; + check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; let mut video_qos = VIDEO_QOS.lock().unwrap(); if video_qos.check_if_updated() { @@ -602,7 +602,7 @@ fn run(sp: GenericService) -> ResultType<()> { if !scrap::is_x11() { if would_block_count >= 100 { super::wayland::release_resource(); - bail!("Wayland capturer none 100 times, try restart captuere"); + bail!("Wayland capturer none 100 times, try restart capture"); } } } @@ -637,7 +637,7 @@ fn run(sp: GenericService) -> ResultType<()> { while wait_begin.elapsed().as_millis() < timeout_millis as _ { check_privacy_mode_changed(&sp, c.privacy_mode_id)?; #[cfg(windows)] - check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?; + check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; frame_controller.try_wait_next(&mut fetched_conn_ids, 300); // break if all connections have received current frame if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 24b3be110..68b9c37cf 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -276,7 +276,7 @@ pub(super) fn get_capturer() -> ResultType { ndisplay: cap_display_info.num, current: cap_display_info.current, privacy_mode_id: 0, - _captuerer_privacy_mode_id: 0, + _capturer_privacy_mode_id: 0, capturer: Box::new(cap_display_info.capturer.clone()), }) } From 380a1670f0d8f1a965cb9e9b956d251fb2ffc733 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:49:36 -0500 Subject: [PATCH 017/734] spelling: chosen Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- .../widgets/kb_layout_type_chooser.dart | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index cfbdb0c4e..58a8f7109 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -6,7 +6,7 @@ import 'package:flutter_hbb/models/platform_model.dart'; import '../../common.dart'; -typedef KBChoosedCallback = Future Function(String); +typedef KBChosenCallback = Future Function(String); const double _kImageMarginVertical = 6.0; const double _kImageMarginHorizontal = 10.0; @@ -25,12 +25,12 @@ const _kKBLayoutImageMap = { class _KBImage extends StatelessWidget { final String kbLayoutType; final double imageWidth; - final RxString choosedType; + final RxString chosenType; const _KBImage({ Key? key, required this.kbLayoutType, required this.imageWidth, - required this.choosedType, + required this.chosenType, }) : super(key: key); @override @@ -40,7 +40,7 @@ class _KBImage extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(_kBorderRadius), border: Border.all( - color: choosedType.value == kbLayoutType + color: chosenType.value == kbLayoutType ? _kImageBorderColor : Colors.transparent, width: _kImageBoarderWidth, @@ -66,13 +66,13 @@ class _KBImage extends StatelessWidget { class _KBChooser extends StatelessWidget { final String kbLayoutType; final double imageWidth; - final RxString choosedType; - final KBChoosedCallback cb; + final RxString chosenType; + final KBChosenCallback cb; const _KBChooser({ Key? key, required this.kbLayoutType, required this.imageWidth, - required this.choosedType, + required this.chosenType, required this.cb, }) : super(key: key); @@ -81,7 +81,7 @@ class _KBChooser extends StatelessWidget { onChanged(String? v) async { if (v != null) { if (await cb(v)) { - choosedType.value = v; + chosenType.value = v; } } } @@ -95,7 +95,7 @@ class _KBChooser extends StatelessWidget { child: _KBImage( kbLayoutType: kbLayoutType, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, ), style: TextButton.styleFrom(padding: EdgeInsets.zero), ), @@ -105,7 +105,7 @@ class _KBChooser extends StatelessWidget { Obx(() => Radio( splashRadius: 0, value: kbLayoutType, - groupValue: choosedType.value, + groupValue: chosenType.value, onChanged: onChanged, )), Text(kbLayoutType), @@ -121,14 +121,14 @@ class _KBChooser extends StatelessWidget { } class KBLayoutTypeChooser extends StatelessWidget { - final RxString choosedType; + final RxString chosenType; final double width; final double height; final double dividerWidth; - final KBChoosedCallback cb; + final KBChosenCallback cb; KBLayoutTypeChooser({ Key? key, - required this.choosedType, + required this.chosenType, required this.width, required this.height, required this.dividerWidth, @@ -147,7 +147,7 @@ class KBLayoutTypeChooser extends StatelessWidget { _KBChooser( kbLayoutType: _kKBLayoutTypeISO, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, cb: cb, ), VerticalDivider( @@ -156,7 +156,7 @@ class KBLayoutTypeChooser extends StatelessWidget { _KBChooser( kbLayoutType: _kKBLayoutTypeNotISO, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, cb: cb, ), ], @@ -208,7 +208,7 @@ showKBLayoutTypeChooser( title: Text('${translate('Select local keyboard type')} ($localPlatform)'), content: KBLayoutTypeChooser( - choosedType: KBLayoutType, + chosenType: KBLayoutType, width: 360, height: 200, dividerWidth: 4.0, From caa557e360784349e9d30e8ed7efe3a79310ac11 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:21 -0500 Subject: [PATCH 018/734] spelling: clipboard Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/clipboard/src/lib.rs | 40 +++++++++++++++++++-------------------- src/clipboard_file.rs | 34 ++++++++++++++++----------------- src/ipc.rs | 4 ++-- src/server/connection.rs | 4 ++-- src/ui_cm_interface.rs | 8 ++++---- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index b992e39e3..e7a533d69 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -21,7 +21,7 @@ pub use context_send::*; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] -pub enum ClipbaordFile { +pub enum ClipboardFile { MonitorReady, FormatList { format_list: Vec<(i32, String)>, @@ -61,8 +61,8 @@ struct ConnEnabled { struct MsgChannel { peer_id: String, conn_id: i32, - sender: UnboundedSender, - receiver: Arc>>, + sender: UnboundedSender, + receiver: Arc>>, } #[derive(PartialEq)] @@ -89,7 +89,7 @@ pub fn get_client_conn_id(peer_id: &str) -> Option { pub fn get_rx_cliprdr_client( peer_id: &str, -) -> (i32, Arc>>) { +) -> (i32, Arc>>) { let mut lock = VEC_MSG_CHANNEL.write().unwrap(); match lock.iter().find(|x| x.peer_id == peer_id.to_owned()) { Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()), @@ -110,7 +110,7 @@ pub fn get_rx_cliprdr_client( } } -pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc>> { +pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc>> { let mut lock = VEC_MSG_CHANNEL.write().unwrap(); match lock.iter().find(|x| x.conn_id == conn_id) { Some(msg_channel) => msg_channel.receiver.clone(), @@ -131,7 +131,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc, conn_id: i32) -> pub fn server_clip_file( context: &mut Box, conn_id: i32, - msg: ClipbaordFile, + msg: ClipboardFile, ) -> u32 { match msg { - ClipbaordFile::MonitorReady => { + ClipboardFile::MonitorReady => { log::debug!("server_monitor_ready called"); let ret = server_monitor_ready(context, conn_id); log::debug!("server_monitor_ready called, return {}", ret); ret } - ClipbaordFile::FormatList { format_list } => { + ClipboardFile::FormatList { format_list } => { log::debug!("server_format_list called"); let ret = server_format_list(context, conn_id, format_list); log::debug!("server_format_list called, return {}", ret); ret } - ClipbaordFile::FormatListResponse { msg_flags } => { + ClipboardFile::FormatListResponse { msg_flags } => { log::debug!("format_list_response called"); let ret = server_format_list_response(context, conn_id, msg_flags); log::debug!("server_format_list_response called, return {}", ret); ret } - ClipbaordFile::FormatDataRequest { + ClipboardFile::FormatDataRequest { requested_format_id, } => { log::debug!("format_data_request called"); @@ -186,7 +186,7 @@ pub fn server_clip_file( log::debug!("server_format_data_request called, return {}", ret); ret } - ClipbaordFile::FormatDataResponse { + ClipboardFile::FormatDataResponse { msg_flags, format_data, } => { @@ -195,7 +195,7 @@ pub fn server_clip_file( log::debug!("server_format_data_response called, return {}", ret); ret } - ClipbaordFile::FileContentsRequest { + ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -221,7 +221,7 @@ pub fn server_clip_file( log::debug!("server_file_contents_request called, return {}", ret); ret } - ClipbaordFile::FileContentsResponse { + ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, @@ -492,7 +492,7 @@ extern "C" fn client_format_list( } conn_id = (*clip_format_list).connID as i32; } - let data = ClipbaordFile::FormatList { format_list }; + let data = ClipboardFile::FormatList { format_list }; // no need to handle result here if conn_id == 0 { VEC_MSG_CHANNEL @@ -519,7 +519,7 @@ extern "C" fn client_format_list_response( conn_id = (*format_list_response).connID as i32; msg_flags = (*format_list_response).msgFlags as i32; } - let data = ClipbaordFile::FormatListResponse { msg_flags }; + let data = ClipboardFile::FormatListResponse { msg_flags }; send_data(conn_id, data); 0 @@ -537,7 +537,7 @@ extern "C" fn client_format_data_request( conn_id = (*format_data_request).connID as i32; requested_format_id = (*format_data_request).requestedFormatId as i32; } - let data = ClipbaordFile::FormatDataRequest { + let data = ClipboardFile::FormatDataRequest { requested_format_id, }; // no need to handle result here @@ -568,7 +568,7 @@ extern "C" fn client_format_data_response( .to_vec(); } } - let data = ClipbaordFile::FormatDataResponse { + let data = ClipboardFile::FormatDataResponse { msg_flags, format_data, }; @@ -614,7 +614,7 @@ extern "C" fn client_file_contents_request( clip_data_id = (*file_contents_request).clipDataId as i32; } - let data = ClipbaordFile::FileContentsRequest { + let data = ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -653,7 +653,7 @@ extern "C" fn client_file_contents_response( .to_vec(); } } - let data = ClipbaordFile::FileContentsResponse { + let data = ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index e6f40e215..f0fe41b8d 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -1,9 +1,9 @@ -use clipboard::ClipbaordFile; +use clipboard::ClipboardFile; use hbb_common::message_proto::*; -pub fn clip_2_msg(clip: ClipbaordFile) -> Message { +pub fn clip_2_msg(clip: ClipboardFile) -> Message { match clip { - ClipbaordFile::MonitorReady => Message { + ClipboardFile::MonitorReady => Message { union: Some(message::Union::Cliprdr(Cliprdr { union: Some(cliprdr::Union::Ready(CliprdrMonitorReady { ..Default::default() @@ -12,7 +12,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatList { format_list } => { + ClipboardFile::FormatList { format_list } => { let mut formats: Vec = Vec::new(); for v in format_list.iter() { formats.push(CliprdrFormat { @@ -32,7 +32,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { ..Default::default() } } - ClipbaordFile::FormatListResponse { msg_flags } => Message { + ClipboardFile::FormatListResponse { msg_flags } => Message { union: Some(message::Union::Cliprdr(Cliprdr { union: Some(cliprdr::Union::FormatListResponse( CliprdrServerFormatListResponse { @@ -44,7 +44,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatDataRequest { + ClipboardFile::FormatDataRequest { requested_format_id, } => Message { union: Some(message::Union::Cliprdr(Cliprdr { @@ -58,7 +58,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatDataResponse { + ClipboardFile::FormatDataResponse { msg_flags, format_data, } => Message { @@ -74,7 +74,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FileContentsRequest { + ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -102,7 +102,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FileContentsResponse { + ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, @@ -123,28 +123,28 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { } } -pub fn msg_2_clip(msg: Cliprdr) -> Option { +pub fn msg_2_clip(msg: Cliprdr) -> Option { match msg.union { - Some(cliprdr::Union::Ready(_)) => Some(ClipbaordFile::MonitorReady), + Some(cliprdr::Union::Ready(_)) => Some(ClipboardFile::MonitorReady), Some(cliprdr::Union::FormatList(data)) => { let mut format_list: Vec<(i32, String)> = Vec::new(); for v in data.formats.iter() { format_list.push((v.id, v.format.clone())); } - Some(ClipbaordFile::FormatList { format_list }) + Some(ClipboardFile::FormatList { format_list }) } - Some(cliprdr::Union::FormatListResponse(data)) => Some(ClipbaordFile::FormatListResponse { + Some(cliprdr::Union::FormatListResponse(data)) => Some(ClipboardFile::FormatListResponse { msg_flags: data.msg_flags, }), - Some(cliprdr::Union::FormatDataRequest(data)) => Some(ClipbaordFile::FormatDataRequest { + Some(cliprdr::Union::FormatDataRequest(data)) => Some(ClipboardFile::FormatDataRequest { requested_format_id: data.requested_format_id, }), - Some(cliprdr::Union::FormatDataResponse(data)) => Some(ClipbaordFile::FormatDataResponse { + Some(cliprdr::Union::FormatDataResponse(data)) => Some(ClipboardFile::FormatDataResponse { msg_flags: data.msg_flags, format_data: data.format_data.into(), }), Some(cliprdr::Union::FileContentsRequest(data)) => { - Some(ClipbaordFile::FileContentsRequest { + Some(ClipboardFile::FileContentsRequest { stream_id: data.stream_id, list_index: data.list_index, dw_flags: data.dw_flags, @@ -156,7 +156,7 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { }) } Some(cliprdr::Union::FileContentsResponse(data)) => { - Some(ClipbaordFile::FileContentsResponse { + Some(ClipboardFile::FileContentsResponse { msg_flags: data.msg_flags, stream_id: data.stream_id, requested_data: data.requested_data.into(), diff --git a/src/ipc.rs b/src/ipc.rs index c562225b4..9048db766 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -9,7 +9,7 @@ use parity_tokio_ipc::{ use serde_derive::{Deserialize, Serialize}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use clipboard::ClipbaordFile; +pub use clipboard::ClipboardFile; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, @@ -191,7 +191,7 @@ pub enum Data { Test, SyncConfig(Option<(Config, Config2)>), #[cfg(not(any(target_os = "android", target_os = "ios")))] - ClipbaordFile(ClipbaordFile), + ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), PrivacyModeState((i32, PrivacyModeState)), TestRendezvousServer, diff --git a/src/server/connection.rs b/src/server/connection.rs index f91281a52..087dbde4c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -319,7 +319,7 @@ impl Connection { allow_err!(conn.stream.send_raw(bytes).await); } #[cfg(windows)] - ipc::Data::ClipbaordFile(_clip) => { + ipc::Data::ClipboardFile(_clip) => { if conn.file_transfer_enabled() { allow_err!(conn.stream.send(&clip_2_msg(_clip)).await); } @@ -1309,7 +1309,7 @@ impl Connection { if self.file_transfer_enabled() { #[cfg(windows)] if let Some(clip) = msg_2_clip(_clip) { - self.send_to_cm(ipc::Data::ClipbaordFile(clip)) + self.send_to_cm(ipc::Data::ClipboardFile(clip)) } } } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 695d60417..a32662d07 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -253,7 +253,7 @@ impl IpcTaskRunner { if !pre_enabled && ContextSend::is_enabled() { allow_err!( self.stream - .send(&Data::ClipbaordFile(clipboard::ClipbaordFile::MonitorReady)) + .send(&Data::ClipboardFile(clipboard::ClipboardFile::MonitorReady)) .await ); } @@ -288,7 +288,7 @@ impl IpcTaskRunner { rx_clip = rx_clip1.lock().await; } else { let rx_clip2; - (_tx_clip, rx_clip2) = unbounded_channel::(); + (_tx_clip, rx_clip2) = unbounded_channel::(); rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); rx_clip = rx_clip1.lock().await; } @@ -354,7 +354,7 @@ impl IpcTaskRunner { } } #[cfg(windows)] - Data::ClipbaordFile(_clip) => { + Data::ClipboardFile(_clip) => { #[cfg(windows)] { let conn_id = self.conn_id; @@ -394,7 +394,7 @@ impl IpcTaskRunner { clip_file = rx_clip.recv() => match clip_file { Some(_clip) => { #[cfg(windows)] - allow_err!(self.tx.send(Data::ClipbaordFile(_clip))); + allow_err!(self.tx.send(Data::ClipboardFile(_clip))); } None => { // From 19046ba8677968a134dd8dd908a1927598a51d61 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:39:49 -0500 Subject: [PATCH 019/734] spelling: colorspace Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/platform/macos.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 70e38eb57..ae996b68a 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -331,7 +331,7 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType { */ let mut colors: Vec = Vec::new(); colors.reserve((size.height * size.width) as usize * 4); - // TIFF is rgb colrspace, no need to convert + // TIFF is rgb colorspace, no need to convert // let cs: id = msg_send![class!(NSColorSpace), sRGBColorSpace]; for y in 0..(size.height as _) { for x in 0..(size.width as _) { From ec8cb0579f58dd9b797db9328cbc446985d4085a Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:22 -0500 Subject: [PATCH 020/734] spelling: common Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/hbb_common/protos/message.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 650e42104..de0d6e7c1 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -503,7 +503,7 @@ message AudioFrame { // Notify peer to show message box. message MessageBox { - // Message type. Refer to flutter/lib/commom.dart/msgBox(). + // Message type. Refer to flutter/lib/common.dart/msgBox(). string msgtype = 1; string title = 2; // English From 5b3835d1fe8f70c53e5a151a8470d25489614b96 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:23 -0500 Subject: [PATCH 021/734] spelling: connecting Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/web/js/src/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/web/js/src/connection.ts b/flutter/web/js/src/connection.ts index 2846d9078..ce6d26684 100644 --- a/flutter/web/js/src/connection.ts +++ b/flutter/web/js/src/connection.ts @@ -82,7 +82,7 @@ export default class Connection { this._ws = ws; this._id = id; console.log( - new Date() + ": Conntecting to rendezvoous server: " + uri + ", for " + id + new Date() + ": Connecting to rendezvoous server: " + uri + ", for " + id ); await ws.open(); console.log(new Date() + ": Connected to rendezvoous server"); From 51f736e84fc49e8f1e5eb8b6dffe9d4b257a42e0 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:22 -0500 Subject: [PATCH 022/734] spelling: connection Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/portable_service.rs | 2 +- src/ui_interface.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 6d2e92ae3..5d96a330e 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -407,7 +407,7 @@ pub mod server { } ConnCount(Some(n)) => { if n == 0 { - log::info!("Connnection count equals 0, exit"); + log::info!("Connection count equals 0, exit"); stream.send(&Data::DataPortableService(WillClose)).await.ok(); break; } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 3b7d1c2c0..c628f0186 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -936,7 +936,7 @@ pub fn account_auth_result() -> String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } -// notice: avoiding create ipc connecton repeatly, +// notice: avoiding create ipc connection repeatly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { From 6ca852363ea28e6c32141064020cd9741d0139d1 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:23 -0500 Subject: [PATCH 023/734] spelling: control Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/scrap/src/common/hwcodec.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index d92ed2a7d..9cd6077a6 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -16,7 +16,7 @@ use hwcodec::{ ffmpeg::{CodecInfo, CodecInfos, DataFormat}, AVPixelFormat, Quality::{self, *}, - RateContorl::{self, *}, + RateControl::{self, *}, }; use std::sync::{Arc, Mutex}; @@ -31,7 +31,7 @@ const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P; pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; const DEFAULT_GOP: i32 = 60; const DEFAULT_HW_QUALITY: Quality = Quality_Default; -const DEFAULT_RC: RateContorl = RC_DEFAULT; +const DEFAULT_RC: RateControl = RC_DEFAULT; pub struct HwEncoder { encoder: Encoder, From 8c901c258528fc8386a0059093f5681837b617ac Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:24 -0500 Subject: [PATCH 024/734] spelling: custom Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/linux/nix_impl.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 47e6d53c0..e2e4bd4a9 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -13,7 +13,7 @@ pub struct Enigo { is_x11: bool, tfc: Option, custom_keyboard: Option, - cutsom_mouse: Option, + custom_mouse: Option, } impl Enigo { @@ -31,7 +31,7 @@ impl Enigo { } /// Set custom mouse. pub fn set_custom_mouse(&mut self, custom_mouse: CustomMouce) { - self.cutsom_mouse = Some(custom_mouse) + self.custom_mouse = Some(custom_mouse) } /// Get custom keyboard. pub fn get_custom_keyboard(&mut self) -> &mut Option { @@ -39,7 +39,7 @@ impl Enigo { } /// Get custom mouse. pub fn get_custom_mouse(&mut self) -> &mut Option { - &mut self.cutsom_mouse + &mut self.custom_mouse } fn tfc_key_down_or_up(&mut self, key: Key, down: bool, up: bool) -> bool { @@ -99,7 +99,7 @@ impl Default for Enigo { None }, custom_keyboard: None, - cutsom_mouse: None, + custom_mouse: None, xdo: EnigoXdo::default(), } } @@ -118,7 +118,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_move_to(x, y); } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_move_to(x, y) } } @@ -127,7 +127,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_move_relative(x, y); } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_move_relative(x, y) } } @@ -136,7 +136,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_down(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_down(button) } else { Ok(()) @@ -147,7 +147,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_up(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_up(button) } } @@ -156,7 +156,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_click(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_click(button) } } @@ -165,7 +165,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_scroll_x(length) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_scroll_x(length) } } @@ -174,7 +174,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_scroll_y(length) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_scroll_y(length) } } From 919e42b1a1657ecbfb955990be24801dfb9bc3a0 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:24 -0500 Subject: [PATCH 025/734] spelling: device Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client/helper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index d38fbf223..e4736c0e8 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -15,7 +15,7 @@ const MIN_LATENCY: i64 = 100; /// Only sync the audio to video, not the other way around. #[derive(Debug)] pub struct LatencyController { - last_video_remote_ts: i64, // generated on remote deivce + last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, } From 7ba932825d4d5f1e95f7b2fef476e457b6234093 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 03:10:42 -0500 Subject: [PATCH 026/734] spelling: distro Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/hbb_common/src/platform/linux.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 4c6375dd7..a6ae2a9e7 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,15 +1,15 @@ use crate::ResultType; lazy_static::lazy_static! { - pub static ref DISTRO: Disto = Disto::new(); + pub static ref DISTRO: Distro = Distro::new(); } -pub struct Disto { +pub struct Distro { pub name: String, pub version_id: String, } -impl Disto { +impl Distro { fn new() -> Self { let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned()) .unwrap_or_default() From 43b975bd355496207e3f2b7a5bb7df0cc5697f7f Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:25 -0500 Subject: [PATCH 027/734] spelling: elapsed Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/cm.tis | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/cm.tis b/src/ui/cm.tis index a1d623322..4e46e217f 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -42,7 +42,7 @@ class Body: Reactor.Component
{c.name}
({c.peer_id})
{auth - ? {disconnected ? translate('Disconnected') : translate('Connected')}{" "}{getElaspsed(c.time, c.now)} + ? {disconnected ? translate('Disconnected') : translate('Connected')}{" "}{getElapsed(c.time, c.now)} : {translate('Request access to your device')}{"..."}}
@@ -442,7 +442,7 @@ function self.ready() { view.move(sw - w, 0, w, h); } -function getElaspsed(time, now) { +function getElapsed(time, now) { var seconds = Date.diff(time, now, #seconds); var hours = seconds / 3600; var days = hours / 24; @@ -482,7 +482,7 @@ function updateTime() { if (el) { var c = connections[body.cur]; if (c && c.authorized && !c.disconnected) { - el.text = getElaspsed(c.time, c.now); + el.text = getElapsed(c.time, c.now); } } updateTime(); From 238de6231f0d1f40b9b1a1d32d3dd52ccb08543c Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:25 -0500 Subject: [PATCH 028/734] spelling: embraced Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- res/lang.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/lang.py b/res/lang.py index 37bbfb3b1..481d65553 100644 --- a/res/lang.py +++ b/res/lang.py @@ -45,7 +45,7 @@ def expand(): if line_strip.startswith('("'): k, v = line_split(line_strip) if k in dict: - # embrased with " to avoid empty v + # embraced with " to avoid empty v line = line.replace('"%s"'%v, '"%s"'%dict[k]) else: line = line.replace(v, "") From db45907e91e1993a3093607dadd799179c024637 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:26 -0500 Subject: [PATCH 029/734] spelling: environment Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/virtual_display/dylib/src/win10/IddController.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/virtual_display/dylib/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h index 767d64798..909f17423 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.h +++ b/libs/virtual_display/dylib/src/win10/IddController.h @@ -134,7 +134,7 @@ const char* GetLastMsg(); * * @param b [in] TRUE to enable printing message. * - * @remark For now, no need to read evironment variable to check if should print. + * @remark For now, no need to read environment variable to check if should print. * */ VOID SetPrintErrMsg(BOOL b); From 53556ba06cde2e47d408b13e1bc61a114f24dcbf Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:26 -0500 Subject: [PATCH 030/734] spelling: essentially Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index fcc2981fd..2794552bd 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -206,7 +206,7 @@ pub trait MouseControllable { /// Click a mouse button /// - /// it's esentially just a consecutive invokation of + /// it's essentially just a consecutive invokation of /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed /// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just /// for From 87e7408cc3cf089b42c0e00b8011216c8c807251 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:27 -0500 Subject: [PATCH 031/734] spelling: exist Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- 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 087dbde4c..9d7470f47 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1857,8 +1857,8 @@ mod privacy_mode { pub(super) fn turn_on_privacy(_conn_id: i32) -> ResultType { #[cfg(windows)] { - let plugin_exitst = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; - Ok(plugin_exitst) + let plugin_exist = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; + Ok(plugin_exist) } #[cfg(not(windows))] { From a58303c8c2bd3dc2820f40bd9e76010654d77704 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:21:09 -0500 Subject: [PATCH 032/734] spelling: github Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- docs/CONTRIBUTING.md | 2 +- flutter/lib/common/widgets/login.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f3165a684..31fd632e6 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -35,7 +35,7 @@ efforts from contributors on the same issue. - Add tests relevant to the fixed bug or new feature. -For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow). +For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). ## Conduct diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index ce27ceb2c..15105ae61 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -538,7 +538,7 @@ Future loginDialog() async { ), LoginWidgetOP( ops: [ - ConfigOP(op: 'Github', iconWidth: 20), + ConfigOP(op: 'GitHub', iconWidth: 20), ConfigOP(op: 'Google', iconWidth: 20), ConfigOP(op: 'Okta', iconWidth: 38), ], From d8a6beccbb6328992f01e2d9c06de1d2c443f057 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:32:18 -0500 Subject: [PATCH 033/734] spelling: holder Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 9d7470f47..0683b4867 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -148,7 +148,7 @@ impl Connection { ..Default::default() }; let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::(); - // holding tx_from_cm_holde to avoid cpu burning of rx_from_cm.recv when all sender closed + // holding tx_from_cm_holder to avoid cpu burning of rx_from_cm.recv when all sender closed let tx_from_cm = tx_from_cm_holder.clone(); let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); From 69595b7b67c3b74cecf9b91c5bec613d25fc7ea1 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:28 -0500 Subject: [PATCH 034/734] spelling: implementation Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/linux/nix_impl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index e2e4bd4a9..d9be7b43b 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -21,7 +21,7 @@ impl Enigo { pub fn delay(&self) -> u64 { self.xdo.delay() } - /// Set delay of xdo implemetation. + /// Set delay of xdo implementation. pub fn set_delay(&mut self, delay: u64) { self.xdo.set_delay(delay) } From c9e5e2cb53e1d25e025c19e27f5c30d529fea423 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:28 -0500 Subject: [PATCH 035/734] spelling: incoming Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client/io_loop.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1f81dfa55..5ec7890be 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -728,11 +728,11 @@ impl Remote { self.handler.adapt_size(); self.send_opts_after_login(peer).await; } - let incomming_format = CodecFormat::from(&vf); - if self.video_format != incomming_format { - self.video_format = incomming_format.clone(); + let incoming_format = CodecFormat::from(&vf); + if self.video_format != incoming_format { + self.video_format = incoming_format.clone(); self.handler.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), + codec_format: Some(incoming_format), ..Default::default() }) }; From fc4d2e4b3ecb9ca1cfeb07f1c0650a3fa1e26113 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:20:20 -0500 Subject: [PATCH 036/734] spelling: into Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/linux/CMakeLists.txt | 2 +- flutter/windows/CMakeLists.txt | 2 +- flutter/windows/runner/win32_window.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index c03d4c576..a9fd84088 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -9,7 +9,7 @@ set(BINARY_NAME "rustdesk") # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.carriez.flutter_hbb") -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# Explicitly opt into modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 5cf603360..926941b84 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -6,7 +6,7 @@ project(rustdesk LANGUAGES CXX) # the on-disk name of your application. set(BINARY_NAME "rustdesk") -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# Explicitly opt into modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) diff --git a/flutter/windows/runner/win32_window.h b/flutter/windows/runner/win32_window.h index 77e52ff01..176791120 100644 --- a/flutter/windows/runner/win32_window.h +++ b/flutter/windows/runner/win32_window.h @@ -31,7 +31,7 @@ class Win32Window { // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function + // consistent size to will treat the width height passed into this function // as logical pixels and scale to appropriate for the default monitor. Returns // true if the window was created successfully. bool CreateAndShow(const std::wstring& title, From f91daf046a4c3415a89b551c46d805f55bd1a524 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:28 -0500 Subject: [PATCH 037/734] spelling: invocation Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index 2794552bd..f55f3dacd 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -206,7 +206,7 @@ pub trait MouseControllable { /// Click a mouse button /// - /// it's essentially just a consecutive invokation of + /// it's essentially just a consecutive invocation of /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed /// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just /// for From f851c5213a53acdd733aa852c4c7822bade616bd Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:21:04 -0500 Subject: [PATCH 038/734] spelling: javascript Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79255e455..bc9bacf19 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Please ensure that you are running these commands from the root of the RustDesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client ## Snapshot From 0fb825000015e1929b86868d44ee24d369aa6604 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:29 -0500 Subject: [PATCH 039/734] spelling: label Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- .../lib/desktop/pages/desktop_setting_page.dart | 4 ++-- flutter/lib/desktop/pages/port_forward_page.dart | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ac92da14c..9f2dc988e 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1436,7 +1436,7 @@ Widget _lock( _LabeledTextField( BuildContext context, - String lable, + String label, TextEditingController controller, String errorText, bool enabled, @@ -1447,7 +1447,7 @@ _LabeledTextField( Expanded( flex: 4, child: Text( - '${translate(lable)}:', + '${translate(label)}:', textAlign: TextAlign.right, style: TextStyle(color: _disabledTextColor(context, enabled)), ), diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index b2458d096..f513a1c6a 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -127,8 +127,8 @@ class _PortForwardPageState extends State } buildTunnel(BuildContext context) { - text(String lable) => Expanded( - child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); + text(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) @@ -241,8 +241,8 @@ class _PortForwardPageState extends State } Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) { - text(String lable) => Expanded( - child: Text(lable, style: const TextStyle(fontSize: 20)) + text(String label) => Expanded( + child: Text(label, style: const TextStyle(fontSize: 20)) .marginOnly(left: _kTextLeftMargin)); return Container( @@ -285,11 +285,11 @@ class _PortForwardPageState extends State } buildRdp(BuildContext context) { - text1(String lable) => Expanded( - child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); - text2(String lable) => Expanded( + text1(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); + text2(String label) => Expanded( child: Text( - lable, + label, style: const TextStyle(fontSize: 20), ).marginOnly(left: _kTextLeftMargin)); return Theme( From 38b5af5362cde55da1c2bb49ba3a73cfa0bb0753 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:29 -0500 Subject: [PATCH 040/734] spelling: latency Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 440c0b0b0..cd05586fc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -825,7 +825,7 @@ impl VideoHandler { /// Handle a new video frame. pub fn handle_frame(&mut self, vf: VideoFrame) -> ResultType { if vf.timestamp != 0 { - // Update the lantency controller with the latest timestamp. + // Update the latency controller with the latest timestamp. self.latency_controller .lock() .unwrap() From 1a5eed576851c5879175ea002e98fd84bae5a830 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:30 -0500 Subject: [PATCH 041/734] spelling: launched Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/macos.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/macos.rs b/src/ui/macos.rs index ab3fb9079..835fd87b0 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -42,7 +42,7 @@ impl DelegateState { } } -static mut LAUCHED: bool = false; +static mut LAUNCHED: bool = false; impl AppHandler for Rc { fn command(&mut self, cmd: u32) { @@ -109,7 +109,7 @@ unsafe fn set_delegate(handler: Option>) { extern "C" fn application_did_finish_launching(_this: &mut Object, _: Sel, _notification: id) { unsafe { - LAUCHED = true; + LAUNCHED = true; } unsafe { let () = msg_send![NSApp(), activateIgnoringOtherApps: YES]; @@ -122,7 +122,7 @@ extern "C" fn application_should_handle_open_untitled_file( _sender: id, ) -> BOOL { unsafe { - if !LAUCHED { + if !LAUNCHED { return YES; } hbb_common::log::debug!("icon clicked on finder"); From 751aa26d8c0657d9a6e7a228b6869d856487f5d0 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:30 -0500 Subject: [PATCH 042/734] spelling: memory Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/portable_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 5d96a330e..4e3d3d1de 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -756,7 +756,7 @@ pub mod client { log::info!("portable service status mismatch"); } if portable_service_running { - log::info!("Create shared memeory capturer"); + log::info!("Create shared memory capturer"); return Ok(Box::new(CapturerPortable::new(current_display, use_yuv))); } else { log::debug!("Create capturer dxgi|gdi"); From 44ead53bc37e633d481a4fc0c36e8b9d030764e5 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:31 -0500 Subject: [PATCH 043/734] spelling: minimized Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/lib/desktop/pages/connection_page.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 85749a256..2dae03250 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -44,7 +44,7 @@ class _ConnectionPageState extends State var svcStatusCode = 0.obs; var svcIsUsingPublicServer = true.obs; - bool isWindowMinisized = false; + bool isWindowMinimized = false; @override void initState() { @@ -80,13 +80,13 @@ class _ConnectionPageState extends State void onWindowEvent(String eventName) { super.onWindowEvent(eventName); if (eventName == 'minimize') { - isWindowMinisized = true; + isWindowMinimized = true; } else if (eventName == 'maximize' || eventName == 'restore') { - if (isWindowMinisized && Platform.isWindows) { - // windows can't update when minisized. + if (isWindowMinimized && Platform.isWindows) { + // windows can't update when minimized. Get.forceAppUpdate(); } - isWindowMinisized = false; + isWindowMinimized = false; } } From ad7640bb0e511751ec746d61fb39e242d297e518 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:20:59 -0500 Subject: [PATCH 044/734] spelling: nonexistent Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/linux/flutter/CMakeLists.txt | 2 +- flutter/windows/flutter/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/linux/flutter/CMakeLists.txt b/flutter/linux/flutter/CMakeLists.txt index d5bd01648..52af0069a 100644 --- a/flutter/linux/flutter/CMakeLists.txt +++ b/flutter/linux/flutter/CMakeLists.txt @@ -70,7 +70,7 @@ target_link_libraries(flutter INTERFACE add_dependencies(flutter flutter_assemble) # === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, +# _phony_ is a nonexistent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( diff --git a/flutter/windows/flutter/CMakeLists.txt b/flutter/windows/flutter/CMakeLists.txt index 930d2071a..b5655b2fa 100644 --- a/flutter/windows/flutter/CMakeLists.txt +++ b/flutter/windows/flutter/CMakeLists.txt @@ -79,7 +79,7 @@ target_include_directories(flutter_wrapper_app PUBLIC add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, +# _phony_ is a nonexistent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") From d6495f3e086c0a63a99a003c66f6f7789b272763 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:31 -0500 Subject: [PATCH 045/734] spelling: pformatetc Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/clipboard/src/windows/wf_cliprdr.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 00ef7254e..a66150c40 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -795,11 +795,11 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryGetData(IDataObject *Thi } static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetCanonicalFormatEtc(IDataObject *This, - FORMATETC *pformatectIn, + FORMATETC *pformatetcIn, FORMATETC *pformatetcOut) { (void)This; - (void)pformatectIn; + (void)pformatetcIn; if (!pformatetcOut) return E_INVALIDARG; From 4993635652b300c349414ea27aa97b6ead371956 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:32 -0500 Subject: [PATCH 046/734] spelling: platforms Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index f55f3dacd..cd2208431 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -19,7 +19,7 @@ //! or any other "special" key on the Linux, macOS and Windows operating system. //! //! Possible use cases could be for testing user interfaces on different -//! plattforms, +//! platforms, //! building remote control applications or just automating tasks for user //! interfaces unaccessible by a public API or scripting language. //! From 33c3489a9eeaf1a0bac7f7c525465eeb325cb4d8 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:58:02 -0500 Subject: [PATCH 047/734] spelling: prefer Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/hbb_common/protos/message.proto | 4 ++-- libs/scrap/src/common/codec.rs | 36 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index de0d6e7c1..e39bc7c6a 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -445,7 +445,7 @@ enum ImageQuality { } message VideoCodecState { - enum PerferCodec { + enum PreferCodec { Auto = 0; VPX = 1; H264 = 2; @@ -455,7 +455,7 @@ message VideoCodecState { int32 score_vpx = 1; int32 score_h264 = 2; int32 score_h265 = 3; - PerferCodec perfer = 4; + PreferCodec prefer = 4; } message OptionMessage { diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 9535e9f3a..acfd4c674 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -23,7 +23,7 @@ use hbb_common::{ use hbb_common::{ config::{Config2, PeerConfig}, lazy_static, - message_proto::video_codec_state::PerferCodec, + message_proto::video_codec_state::PreferCodec, }; #[cfg(feature = "hwcodec")] @@ -149,29 +149,29 @@ impl Encoder { && states.iter().all(|(_, s)| s.score_h265 > 0); // Preference first - let mut preference = PerferCodec::Auto; + let mut preference = PreferCodec::Auto; let preferences: Vec<_> = states .iter() .filter(|(_, s)| { - s.perfer == PerferCodec::VPX.into() - || s.perfer == PerferCodec::H264.into() && enabled_h264 - || s.perfer == PerferCodec::H265.into() && enabled_h265 + s.prefer == PreferCodec::VPX.into() + || s.prefer == PreferCodec::H264.into() && enabled_h264 + || s.prefer == PreferCodec::H265.into() && enabled_h265 }) - .map(|(_, s)| s.perfer) + .map(|(_, s)| s.prefer) .collect(); if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) { - preference = preferences[0].enum_value_or(PerferCodec::Auto); + preference = preferences[0].enum_value_or(PreferCodec::Auto); } match preference { - PerferCodec::VPX => *name.lock().unwrap() = None, - PerferCodec::H264 => { + PreferCodec::VPX => *name.lock().unwrap() = None, + PreferCodec::H264 => { *name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name)) } - PerferCodec::H265 => { + PreferCodec::H265 => { *name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name)) } - PerferCodec::Auto => { + PreferCodec::Auto => { // score encoder let mut score_vpx = SCORE_VPX; let mut score_h264 = best.h264.as_ref().map_or(0, |c| c.score); @@ -252,7 +252,7 @@ impl Decoder { score_vpx: SCORE_VPX, score_h264: best.h264.map_or(0, |c| c.score), score_h265: best.h265.map_or(0, |c| c.score), - perfer: Self::codec_preference(_id).into(), + prefer: Self::codec_preference(_id).into(), ..Default::default() }; } @@ -272,7 +272,7 @@ impl Decoder { score_vpx: SCORE_VPX, score_h264, score_h265, - perfer: Self::codec_preference(_id).into(), + prefer: Self::codec_preference(_id).into(), ..Default::default() }; } @@ -405,19 +405,19 @@ impl Decoder { } #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] - fn codec_preference(id: &str) -> PerferCodec { + fn codec_preference(id: &str) -> PreferCodec { let codec = PeerConfig::load(id) .options .get("codec-preference") .map_or("".to_owned(), |c| c.to_owned()); if codec == "vp9" { - PerferCodec::VPX + PreferCodec::VPX } else if codec == "h264" { - PerferCodec::H264 + PreferCodec::H264 } else if codec == "h265" { - PerferCodec::H265 + PreferCodec::H265 } else { - PerferCodec::Auto + PreferCodec::Auto } } } From 1011568fc12664ba320d0a508434842c5c356617 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:32 -0500 Subject: [PATCH 048/734] spelling: privileged Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/platform/macos.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index ae996b68a..204993c13 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -440,7 +440,7 @@ pub fn start_os_service() { .status() .ok(); println!("The others killed"); - // launchctl load/unload/start agent not work in daemon, show not priviledged. + // launchctl load/unload/start agent not work in daemon, show not privileged. // sudo launchctl asuser 501 open -n also not allowed. std::process::Command::new("launchctl") .args(&[ From 958c0ad18b94205e482fd5156a903fb132dba765 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:33 -0500 Subject: [PATCH 049/734] spelling: receiving Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client/io_loop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5ec7890be..71d353c88 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -915,7 +915,7 @@ impl Remote { } }, Err(err) => { - println!("error recving digest: {}", err); + println!("error receiving digest: {}", err); } } } From c89e104f3ec533e7fd1782059f17bd33275661ea Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:33 -0500 Subject: [PATCH 050/734] spelling: regardless Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index cd2208431..509bbf97c 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -468,7 +468,7 @@ pub trait KeyboardControllable { /// Emits keystrokes such that the given string is inputted. /// /// You can use many unicode here like: ❤️. This works - /// regadless of the current keyboardlayout. + /// regardless of the current keyboardlayout. /// /// # Example /// From f43b8b23a88ed711b02188277495596e70f30324 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:34 -0500 Subject: [PATCH 051/734] spelling: registrar Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/windows/runner/win32_window.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp index 2ff6d686c..c15819df0 100644 --- a/flutter/windows/runner/win32_window.cpp +++ b/flutter/windows/runner/win32_window.cpp @@ -43,7 +43,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); From 996ed5398e8fdfb037d736e34a3af5e990e277d8 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:34 -0500 Subject: [PATCH 052/734] spelling: rendezvous Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/web/js/src/connection.ts | 4 ++-- src/rendezvous_mediator.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/web/js/src/connection.ts b/flutter/web/js/src/connection.ts index ce6d26684..b0c479c90 100644 --- a/flutter/web/js/src/connection.ts +++ b/flutter/web/js/src/connection.ts @@ -82,10 +82,10 @@ export default class Connection { this._ws = ws; this._id = id; console.log( - new Date() + ": Connecting to rendezvoous server: " + uri + ", for " + id + new Date() + ": Connecting to rendezvous server: " + uri + ", for " + id ); await ws.open(); - console.log(new Date() + ": Connected to rendezvoous server"); + console.log(new Date() + ": Connected to rendezvous server"); const conn_type = rendezvous.ConnType.DEFAULT_CONN; const nat_type = rendezvous.NatType.SYMMETRIC; const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({ diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 73c017e2e..8b7dae1ba 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -469,10 +469,10 @@ impl RendezvousMediator { Ok(()) } - fn get_relay_server(&self, provided_by_rendzvous_server: String) -> String { + fn get_relay_server(&self, provided_by_rendezvous_server: String) -> String { let mut relay_server = Config::get_option("relay-server"); if relay_server.is_empty() { - relay_server = provided_by_rendzvous_server; + relay_server = provided_by_rendezvous_server; } if relay_server.is_empty() { relay_server = crate::increase_port(&self.host, 1); From 37b0ac6e479d6a49acef237f9e38daad5999837c Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:35 -0500 Subject: [PATCH 053/734] spelling: repeatedly Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui_interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index c628f0186..9984198b8 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -936,7 +936,7 @@ pub fn account_auth_result() -> String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } -// notice: avoiding create ipc connection repeatly, +// notice: avoiding create ipc connection repeatedly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { From a0b73b96da2cd8939337c3ade34dafe52a0a80ec Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:35 -0500 Subject: [PATCH 054/734] spelling: responds Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/windows/runner/win32_window.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/windows/runner/win32_window.h b/flutter/windows/runner/win32_window.h index 176791120..94a7bcd56 100644 --- a/flutter/windows/runner/win32_window.h +++ b/flutter/windows/runner/win32_window.h @@ -77,7 +77,7 @@ class Win32Window { // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, From 3949e3162c4c66e1f34c5ecd304f4119c16d647e Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:36 -0500 Subject: [PATCH 055/734] spelling: rotation Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/scrap/src/dxgi/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 5829686b5..152f502a3 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -262,7 +262,7 @@ impl Capturer { _ => { return Err(io::Error::new( io::ErrorKind::Other, - "Unknown roration".to_string(), + "Unknown rotation".to_string(), )); } }; From d6bc1d4b8ad593fe55c73f7012b72f8b124a8c50 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:36 -0500 Subject: [PATCH 056/734] spelling: selection Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 0683b4867..cbd130233 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -593,7 +593,7 @@ impl Connection { Some(data) = rx_from_cm.recv() => { match data { ipc::Data::Close => { - bail!("Close requested from selfection manager"); + bail!("Close requested from selection manager"); } _ => {} } From 7b047a32abd14e4423d5f6ff8e225d0cdfc1d246 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:37 -0500 Subject: [PATCH 057/734] spelling: separate Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/portable_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 4e3d3d1de..6b87da21b 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -203,7 +203,7 @@ mod utils { } } -// functions called in seperate SYSTEM user process. +// functions called in separate SYSTEM user process. pub mod server { use super::*; From e29866edc97128e09bb6ac30c77627ca26149d07 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:37 -0500 Subject: [PATCH 058/734] spelling: separated Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/lang/en.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/en.rs b/src/lang/en.rs index b718fc0f9..14d221ef3 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("setup_server_tip", "For faster connection, please set up your own server"), ("Auto Login", "Auto Login (Only valid if you set \"Lock after session end\")"), ("whitelist_tip", "Only whitelisted IP can access me"), - ("whitelist_sep", "Seperated by comma, semicolon, spaces or new line"), + ("whitelist_sep", "Separated by comma, semicolon, spaces or new line"), ("Wrong credentials", "Wrong username or password"), ("invalid_http", "must start with http:// or https://"), ("install_daemon_tip", "For starting on boot, you need to install system service."), From a6b672848befd0427a7488309aa970158530c2ba Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:38 -0500 Subject: [PATCH 059/734] spelling: settings Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/platform/linux.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 3eb8f0b87..0e4f98d52 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -707,9 +707,9 @@ pub fn get_double_click_time() -> u32 { unsafe { let mut double_click_time = 0u32; let property = std::ffi::CString::new("gtk-double-click-time").unwrap(); - let setings = gtk_settings_get_default(); + let settings = gtk_settings_get_default(); g_object_get( - setings, + settings, property.as_ptr(), &mut double_click_time as *mut u32, 0 as *const libc::c_void, From 5e4ca9ef927662e3239b6c25902fe7b51977c41a Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:38 -0500 Subject: [PATCH 060/734] spelling: valid Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/portable/src/bin_reader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index 2d0b1bf7e..0a6cd8ef9 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -74,7 +74,7 @@ impl BinaryReader { assert!(BIN_DATA.len() > IDENTIFIER_LENGTH, "bin data invalid!"); let mut iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]); if iden != "rustdesk" { - panic!("bin file is not vaild!"); + panic!("bin file is not valid!"); } base += IDENTIFIER_LENGTH; loop { From 128aa17476e40d333a9aa213cc0025f283bb1c24 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:39 -0500 Subject: [PATCH 061/734] spelling: visible Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/lib/desktop/widgets/tabbar_widget.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 91ce6cce6..d428bcb9b 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -906,7 +906,7 @@ class _TabState extends State<_Tab> with RestorationMixin { children: [ _buildTabContent(), Obx((() => _CloseButton( - visiable: hover.value && widget.closable, + visible: hover.value && widget.closable, tabSelected: isSelected, onClose: () => widget.onClose(), ))) @@ -938,13 +938,13 @@ class _TabState extends State<_Tab> with RestorationMixin { } class _CloseButton extends StatelessWidget { - final bool visiable; + final bool visible; final bool tabSelected; final Function onClose; const _CloseButton({ Key? key, - required this.visiable, + required this.visible, required this.tabSelected, required this.onClose, }) : super(key: key); @@ -954,7 +954,7 @@ class _CloseButton extends StatelessWidget { return SizedBox( width: _kIconSize, child: Offstage( - offstage: !visiable, + offstage: !visible, child: InkWell( customBorder: const RoundedRectangleBorder(), onTap: () => onClose(), From cd921987e9558b32d79f80ffb78966345a9c2a49 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:39 -0500 Subject: [PATCH 062/734] spelling: whitelist Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- 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 cbd130233..c29faa724 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -663,7 +663,7 @@ impl Connection { self.send_login_error("Your ip is blocked by the peer") .await; Self::post_alarm_audit( - AlarmAuditType::IpWhiltelist, //"ip whiltelist", + AlarmAuditType::IpWhitelist, //"ip whitelist", true, json!({ "ip":addr.ip(), @@ -1875,7 +1875,7 @@ struct ConnAuditResponse { } pub enum AlarmAuditType { - IpWhiltelist = 0, + IpWhitelist = 0, ManyWrongPassword = 1, FrequentAttempt = 2, } From 21438894040fca679f4724ca9f715ecd7e08e647 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 11:09:08 +0800 Subject: [PATCH 063/734] fix win scancode filter, ignore 0xE0.. Signed-off-by: fufesou --- src/keyboard.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index f29eb27bc..a11b0e97e 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -617,7 +617,8 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { // https://github.com/rustdesk/rustdesk/issues/1371 - if event.scan_code > 255 { + // Filter scancodes that are greater than 255 and the hight word is not 0xE0. + if event.scan_code > 255 && (event.scan_code >> 8) != 0xE0 { return None; } event.scan_code From 6f455be347dd2954c5da78bd00323b2985c869dd Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 8 Jan 2023 16:27:03 +0800 Subject: [PATCH 064/734] opt: set title for all windows --- flutter/lib/main.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6fd205a22..6593f1804 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -61,6 +61,8 @@ Future main(List args) async { kAppTypeDesktopRemote, 'RustDesk - Remote Desktop', ); + WindowController.fromWindowId(windowId!) + .setTitle('RustDesk - Remote Desktop'); break; case WindowType.FileTransfer: desktopType = DesktopType.fileTransfer; @@ -69,6 +71,8 @@ Future main(List args) async { kAppTypeDesktopFileTransfer, 'RustDesk - File Transfer', ); + WindowController.fromWindowId(windowId!) + .setTitle('RustDesk - File Transfer'); break; case WindowType.PortForward: desktopType = DesktopType.portForward; @@ -135,6 +139,7 @@ void runMainApp(bool startService) async { windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.setOpacity(1); }); + windowManager.setTitle("RustDesk"); } void runMobileApp() async { @@ -198,6 +203,7 @@ void runMultiWindow( } // show window from hidden status WindowController.fromWindowId(windowId!).show(); + WindowController.fromWindowId(windowId!).setTitle(title); } void runConnectionManagerScreen(bool hide) async { From 3f3e71c1f9f48a6dbfb731626bac150118b2d827 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 8 Jan 2023 17:42:18 +0800 Subject: [PATCH 065/734] feat: add arm64 appimage build --- .github/workflows/flutter-nightly.yml | 41 ++++++++- appimage/AppImageBuilder-aarch64.yml | 85 +++++++++++++++++++ ...Builder.yml => AppImageBuilder-x86_64.yml} | 0 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 appimage/AppImageBuilder-aarch64.yml rename appimage/{AppImageBuilder.yml => AppImageBuilder-x86_64.yml} (100%) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index eb13edf15..aaafede07 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -761,6 +761,13 @@ jobs: use-cross: true, extra-build-features: "", } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } # - { # arch: armv7, @@ -939,6 +946,13 @@ jobs: use-cross: true, extra-build-features: "", } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } # - { # arch: aarch64, # target: aarch64-unknown-linux-gnu, @@ -1046,7 +1060,7 @@ jobs: shell: bash run: | for name in rustdesk*??.deb; do - mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" done - name: Publish debian package @@ -1057,6 +1071,29 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - name: Upload Artifact uses: actions/upload-artifact@master @@ -1310,7 +1347,7 @@ jobs: popd # run appimage-builder pushd appimage - sudo appimage-builder --skip-tests + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml - name: Publish appimage package if: ${{ matrix.job.extra-build-features == 'appimage' }} diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml new file mode 100644 index 000000000..12d737718 --- /dev/null +++ b/appimage/AppImageBuilder-aarch64.yml @@ -0,0 +1,85 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +version: 1 +script: + - rm -rf ./AppDir || true + - bsdtar -zxvf ../rustdesk-1.2.0.deb + - tar -xvf ./data.tar.xz + - mkdir ./AppDir + - mv ./usr ./AppDir/usr + # 32x32 icon + - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done + # desktop file + # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop + - rm -rf ./AppDir/usr/share/applications +AppDir: + path: ./AppDir + app_info: + id: rustdesk + name: rustdesk + icon: rustdesk + version: 1.2.0 + exec: usr/lib/rustdesk/rustdesk + exec_args: $@ + apt: + arch: + - aarch64 + allow_unauthenticated: true + sources: + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic main restricted + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates main restricted + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic universe + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates universe + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic multiverse + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates multiverse + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-backports main restricted + universe multiverse + include: + - libc6:amd64 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva-drm2 + - libva-x11-2 + - libvdpau1 + - libgstreamer-plugins-base1.0-0 + exclude: + - humanity-icon-theme + - hicolor-icon-theme + - adwaita-icon-theme + - ubuntu-mono + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + runtime: + env: + GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-xenial: + image: appimagecrafters/tests-env:ubuntu-xenial + command: ./AppRun +AppImage: + arch: aarch64 + update-information: guess diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder-x86_64.yml similarity index 100% rename from appimage/AppImageBuilder.yml rename to appimage/AppImageBuilder-x86_64.yml From 522aacb9b5c62d21861a221d5e110ae04acd90a3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 9 Jan 2023 10:38:43 +0800 Subject: [PATCH 066/734] fix: missing deps fix: trusted sourceline fix: libc6 arm64 def fix: aarch64 appimage build fix: change to ports ubuntu --- .github/workflows/flutter-nightly.yml | 2 +- appimage/AppImageBuilder-aarch64.yml | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index aaafede07..e1dfdbe74 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -976,7 +976,7 @@ jobs: - name: Prepare env run: | sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools mkdir -p ./target/release/ - name: Restore the rustdesk lib file diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 12d737718..ee37baf1c 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -22,19 +22,18 @@ AppDir: exec_args: $@ apt: arch: - - aarch64 + - arm64 allow_unauthenticated: true sources: - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic main restricted - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates main restricted - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic universe - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates universe - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic multiverse - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates multiverse - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-backports main restricted + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' include: - - libc6:amd64 + - libc6 - libgtk-3-0 - libxcb-randr0 - libxdo3 From 6a9dbbd7a0cdf2c24f9960c5f071adf686e103f1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 10 Jan 2023 11:07:12 +0800 Subject: [PATCH 067/734] fix: remove gio module dir fix: set GDK_BACKEND to x11 --- appimage/AppImageBuilder-aarch64.yml | 1 + appimage/AppImageBuilder-x86_64.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index ee37baf1c..f3cd8f568 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -63,6 +63,7 @@ AppDir: runtime: env: GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + GDK_BACKEND: x11 test: fedora-30: image: appimagecrafters/tests-env:fedora-30 diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index ae95fd2ce..59dd5164f 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -66,6 +66,7 @@ AppDir: runtime: env: GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + GDK_BACKEND: x11 test: fedora-30: image: appimagecrafters/tests-env:fedora-30 From dac476bce08e487ecfc1034747c7f0c160ab586d Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 10 Jan 2023 12:57:56 +0800 Subject: [PATCH 068/734] update hwcodec spell Signed-off-by: 21pages --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 659702704..86707fd62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2602,7 +2602,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#e819484c4c010199f2a0977bdf306b4edbeafbae" +source = "git+https://github.com/21pages/hwcodec#64f885b3787694b16dfcff08256750b0376b2eba" dependencies = [ "bindgen 0.59.2", "cc", From b8e68475d8de1e5473eef0ac54665c23abc6eb7e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Jan 2023 13:01:19 +0800 Subject: [PATCH 069/734] full flutter ci --- .github/workflows/flutter-ci.yml | 995 +++++++++++++++++++++++++++++-- 1 file changed, 961 insertions(+), 34 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 7825286bd..df47d07d1 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -1,4 +1,4 @@ -name: Flutter CI +name: Full Flutter CI on: workflow_dispatch: @@ -11,42 +11,142 @@ on: paths-ignore: - ".github/**" +env: + LLVM_VERSION: "10.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + jobs: - build: + build-for-windows: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) runs-on: ${{ matrix.job.os }} strategy: - fail-fast: false + fail-fast: true matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - # - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - # - { target: x86_64-pc-windows-msvc , os: windows-2019 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: - name: Checkout source code uses: actions/checkout@v3 - - name: Install prerequisites - shell: bash - run: | - case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; - # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; - esac + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + build-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-latest, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Import the codesign cert + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm yasm cmake gcc wget ninja + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -56,14 +156,21 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} - name: Install flutter rust bridge deps + shell: bash run: | dart pub global activate ffigen --version 5.0.1 # flutter_rust_bridge - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd /tmp + wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz + tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz + mkdir -p ~/.cargo/bin + mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen + popd pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -71,29 +178,849 @@ jobs: uses: lukka/run-vcpkg@v7 with: setupOnly: true - vcpkgGitCommitId: '1d4128f08e30cec31b94500840c7eca8ebc579cb' + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash - - name: Show version information (Rust, cargo, GCC) + - name: Show version information (Rust, cargo, Clang) shell: bash run: | - gcc --version || true + clang --version || true rustup -V rustup toolchain list rustup default cargo -V rustc -V - - name: Build rustdesk ffi lib - run: cargo build --features flutter --lib - - - name: Build Flutter + - name: Build rustdesk run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + # - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + case "${{ matrix.job.arch }}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + ;; + aarch64|armv7) + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool + esac + cmake --version + gcc -v + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + case "${{ matrix.job.arch }}" in + x86_64) + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + ;; + aarch64|armv7) + pushd /artifacts + # libyuv + git clone https://chromium.googlesource.com/libyuv/libyuv || true + pushd libyuv + git pull + mkdir -p build + pushd build + mkdir -p /artifacts/vcpkg/installed + cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed + make -j4 && make install + popd + popd + # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC + wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz + tar -zxvf opus.tar.gz; ls -l + pushd opus-1.1.2 + ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed + make -j4; make install + ;; + esac + - name: Upload artifacts + uses: actions/upload-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: | + /opt/artifacts/vcpkg/installed + + generate-bridge-linux: + name: generate bridge + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install prerequisites + run: | + sudo apt update -y + sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + workspace: "/tmp/flutter_rust_bridge/frb_codegen" + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v3 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install ffigen + run: | + dart pub global activate ffigen --version 5.0.1 + + - name: Install flutter rust bridge deps + shell: bash + run: | + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + + - name: Run flutter rust bridge + run: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Upload Artifact + uses: actions/upload-artifact@master + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + + build-rustdesk-android-arm64: + needs: [generate-bridge-linux] + name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + arch: x86_64, + target: aarch64-linux-android, + os: ubuntu-18.04, + extra-build-features: "", + } + # - { + # arch: x86_64, + # target: armv7-linux-androideabi, + # os: ubuntu-18.04, + # extra-build-features: "", + # } + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r22b + add-to-path: true + + - name: Download deps + shell: bash + run: | + pushd /opt + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz + tar xzf dep.tar.gz + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + VCPKG_ROOT: /opt/vcpkg + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # download so pushd flutter - flutter pub get - flutter build linux --debug -v + wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzvf so.tar.gz popd + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + + build-rustdesk-lib-linux-amd64: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + # use a high level qemu-user-static + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + # not ready yet + # distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + 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 + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-lib-linux-arm: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + # use a high level qemu-user-static + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { + # arch: armv7, + # target: arm-unknown-linux-gnueabihf, + # os: ubuntu-20.04, + # use-cross: true, + # extra-build-features: "", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + aarch64) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + ls -l /opt/artifacts/vcpkg/installed/lib/ + mkdir -p /vcpkg/installed/arm64-linux + ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib + 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 + ;; + armv7) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + mkdir -p /vcpkg/installed/arm-linux + ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib + 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 + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-linux-arm: + needs: [build-rustdesk-lib-linux-arm] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 # 20.04 has more performance on arm build + strategy: + fail-fast: true + matrix: + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { + # arch: aarch64, + # target: aarch64-unknown-linux-gnu, + # os: ubuntu-18.04, # just for naming package, not running host + # use-cross: true, + # extra-build-features: "flatpak", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - name: Download Flutter + shell: bash + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /opt + # clone repo and reset to flutter 3.0.5 + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + # reset to flutter 3.0.5 + git fetch + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + popd + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/flutter-elinux:/opt/flutter-elinux" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # we use flutter-elinux to build our rustdesk + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH + flutter-elinux doctor -v + # edit to corresponding arch + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + armv7) + sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py + sed -i "s/x64\/release/arm\/release/g" ./build.py + ;; + esac + python3 ./build.py --flutter --hwcodec --skip-cargo + + build-rustdesk-linux-amd64: + needs: [build-rustdesk-lib-linux-amd64] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 + strategy: + fail-fast: true + matrix: + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + ls -l . + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + pushd /workspace + python3 ./build.py --flutter --hwcodec --skip-cargo From fae8a9489159fcf9bf2afcc3f418c272d4054f1b Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 28 Dec 2022 11:01:09 +0800 Subject: [PATCH 070/734] sync strategy Signed-off-by: 21pages --- src/hbbs_http.rs | 1 + src/hbbs_http/sync.rs | 132 +++++++++++++++++++++++++++++++++++++++ src/server.rs | 10 ++- src/server/connection.rs | 64 ++++++------------- 4 files changed, 160 insertions(+), 47 deletions(-) create mode 100644 src/hbbs_http/sync.rs diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 08ad36eb9..76ced87a0 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -5,6 +5,7 @@ use serde_json::{Map, Value}; #[cfg(feature = "flutter")] pub mod account; pub mod record_upload; +pub mod sync; #[derive(Debug)] pub enum HbbHttpResponse { diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs new file mode 100644 index 000000000..9497cc449 --- /dev/null +++ b/src/hbbs_http/sync.rs @@ -0,0 +1,132 @@ +use std::{collections::HashMap, sync::Mutex, time::Duration}; + +use hbb_common::{ + config::{Config, LocalConfig}, + tokio::{self, sync::broadcast, time::Instant}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +use crate::Connection; + +const TIME_HEARTBEAT: Duration = Duration::from_secs(30); +const TIME_CONN: Duration = Duration::from_secs(3); + +lazy_static::lazy_static! { + static ref SENDER : Mutex>> = Mutex::new(start_hbbs_sync()); +} + +pub fn start() { + let _sender = SENDER.lock().unwrap(); +} + +pub fn signal_receiver() -> broadcast::Receiver> { + SENDER.lock().unwrap().subscribe() +} + +fn start_hbbs_sync() -> broadcast::Sender> { + let (tx, _rx) = broadcast::channel::>(16); + std::thread::spawn(move || start_hbbs_sync_async()); + return tx; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StrategyOptions { + pub config_options: HashMap, + pub extra: HashMap, +} + +#[tokio::main(flavor = "current_thread")] +async fn start_hbbs_sync_async() { + tokio::spawn(async move { + let mut interval = tokio::time::interval_at(Instant::now() + TIME_CONN, TIME_CONN); + let mut last_send = Instant::now(); + loop { + tokio::select! { + _ = interval.tick() => { + let url = heartbeat_url(); + let modified_at = LocalConfig::get_option("strategy_timestamp").parse::().unwrap_or(0); + if !url.is_empty() { + let conns = Connection::alive_conns(); + if conns.is_empty() && last_send.elapsed() < TIME_HEARTBEAT { + continue; + } + last_send = Instant::now(); + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + if !conns.is_empty() { + v["conns"] = json!(conns); + } + v["modified_at"] = json!(modified_at); + if let Ok(s) = crate::post_request(url.clone(), v.to_string(), "").await { + if let Ok(mut rsp) = serde_json::from_str::>(&s) { + if let Some(conns) = rsp.remove("disconnect") { + if let Ok(conns) = serde_json::from_value::>(conns) { + SENDER.lock().unwrap().send(conns).ok(); + } + } + if let Some(rsp_modified_at) = rsp.remove("modified_at") { + if let Ok(rsp_modified_at) = serde_json::from_value::(rsp_modified_at) { + if rsp_modified_at != modified_at { + LocalConfig::set_option("strategy_timestamp".to_string(), rsp_modified_at.to_string()); + } + } + } + if let Some(strategy) = rsp.remove("strategy") { + if let Ok(strategy) = serde_json::from_value::(strategy) { + handle_config_options(strategy.config_options); + } + } + } + } + } + } + } + } + }) + .await + .ok(); +} + +fn heartbeat_url() -> String { + let url = crate::common::get_api_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ); + if url.is_empty() || url.contains("rustdesk.com") { + return "".to_owned(); + } + format!("{}/api/heartbeat", url) +} + +fn handle_config_options(config_options: HashMap) { + let map = HashMap::from([ + ("enable-keyboard", ""), + ("enable-clipboard", ""), + ("enable-file-transfer", ""), + ("enable-audio", ""), + ("enable-tunnel", ""), + ("enable-remote-restart", ""), + ("enable-record-session", ""), + ("allow-remote-config-modification", ""), + ("approve-mode", ""), + ("verification-method", "use-both-passwords"), + ("enable-rdp", ""), + ("enable-lan-discovery", ""), + ("direct-server", ""), + ("direct-access-port", ""), + ]); + let mut options = Config::get_options(); + for (k, v) in map { + if let Some(v2) = config_options.get(k) { + if v == v2 { + options.remove(k); + } else { + options.insert(k.to_string(), v2.to_string()); + } + } else { + options.remove(k); + } + } + Config::set_options(options); +} diff --git a/src/server.rs b/src/server.rs index 5c020261f..381e3df90 100644 --- a/src/server.rs +++ b/src/server.rs @@ -194,9 +194,14 @@ pub async fn create_tcp_connection( } } - #[cfg(target_os = "macos")]{ + #[cfg(target_os = "macos")] + { use std::process::Command; - Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().ok(); + Command::new("/usr/bin/caffeinate") + .arg("-u") + .arg("-t 5") + .spawn() + .ok(); log::info!("wake up macos"); } Connection::start(addr, stream, id, Arc::downgrade(&server)).await; @@ -385,6 +390,7 @@ pub async fn start_server(is_server: bool) { #[cfg(windows)] crate::platform::windows::bootstrap(); input_service::fix_key_down_timeout_loop(); + crate::hbbs_http::sync::start(); #[cfg(target_os = "linux")] if crate::platform::current_is_wayland() { allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); diff --git a/src/server/connection.rs b/src/server/connection.rs index c29faa724..9764820f3 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -26,7 +26,6 @@ use hbb_common::{ }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::call_main_service_mouse_input; -use serde::Deserialize; use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -40,6 +39,7 @@ pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { static ref LOGIN_FAILURES: Arc::>> = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); + static ref ALIVE_CONNS: Arc::>> = Default::default(); } pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); @@ -74,7 +74,6 @@ pub struct Connection { read_jobs: Vec, timer: Interval, file_timer: Interval, - http_timer: Interval, file_transfer: Option<(String, bool)>, port_forward_socket: Option>, port_forward_address: String, @@ -147,6 +146,7 @@ impl Connection { challenge: Config::get_auto_password(6), ..Default::default() }; + ALIVE_CONNS.lock().unwrap().push(id); let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::(); // holding tx_from_cm_holder to avoid cpu burning of rx_from_cm.recv when all sender closed let tx_from_cm = tx_from_cm_holder.clone(); @@ -154,7 +154,7 @@ impl Connection { let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_input, rx_input) = std_mpsc::channel(); - let (tx_stop, mut rx_stop) = mpsc::unbounded_channel::(); + let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); let mut conn = Self { @@ -169,7 +169,6 @@ impl Connection { read_jobs: Vec::new(), timer: time::interval(SEC30), file_timer: time::interval(SEC30), - http_timer: time::interval(Duration::from_secs(3)), file_transfer: None, port_forward_socket: None, port_forward_address: "".to_owned(), @@ -393,12 +392,11 @@ impl Connection { conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30); } } - _ = conn.http_timer.tick() => { - Connection::post_heartbeat(conn.server_audit_conn.clone(), conn.inner.id, tx_stop.clone()); - }, - Some(reason) = rx_stop.recv() => { - conn.on_close_manually(&reason, &reason).await; - + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&id) { + conn.on_close_manually("web console", "web console").await; + break; + } } Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { @@ -514,6 +512,7 @@ impl Connection { conn.post_conn_audit(json!({ "action": "close", })); + ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); log::info!("#{} connection loop exited", id); } @@ -584,10 +583,10 @@ impl Connection { rx_from_cm: &mut mpsc::UnboundedReceiver, ) -> ResultType<()> { let mut last_recv_time = Instant::now(); - let (tx_stop, mut rx_stop) = mpsc::unbounded_channel::(); if let Some(mut forward) = self.port_forward_socket.take() { log::info!("Running port forwarding loop"); self.stream.set_raw(); + let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); loop { tokio::select! { Some(data) = rx_from_cm.recv() => { @@ -618,10 +617,12 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } - Connection::post_heartbeat(self.server_audit_conn.clone(), self.inner.id, tx_stop.clone()); } - Some(reason) = rx_stop.recv() => { - bail!(reason); + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&self.inner.id) { + // todo: check reconnect + bail!("Closed manually by the web console"); + } } } } @@ -711,30 +712,6 @@ impl Connection { }); } - fn post_heartbeat( - server_audit_conn: String, - conn_id: i32, - tx_stop: mpsc::UnboundedSender, - ) { - if server_audit_conn.is_empty() { - return; - } - let url = server_audit_conn.clone(); - let mut v = Value::default(); - v["id"] = json!(Config::get_id()); - v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); - v["conn_id"] = json!(conn_id); - tokio::spawn(async move { - if let Ok(rsp) = Self::post_audit_async(url, v).await { - if let Ok(rsp) = serde_json::from_str::(&rsp) { - if rsp.action == "disconnect" { - tx_stop.send("web console".to_string()).ok(); - } - } - } - }); - } - fn post_file_audit( &self, r#type: FileAuditType, @@ -1710,6 +1687,10 @@ impl Connection { async fn send(&mut self, msg: Message) { allow_err!(self.stream.send(&msg).await); } + + pub fn alive_conns() -> Vec { + ALIVE_CONNS.lock().unwrap().clone() + } } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1867,13 +1848,6 @@ mod privacy_mode { } } -#[derive(Debug, Deserialize)] -struct ConnAuditResponse { - #[allow(dead_code)] - ret: bool, - action: String, -} - pub enum AlarmAuditType { IpWhitelist = 0, ManyWrongPassword = 1, From ad8e3c7555d7236501d78b855b68ed04aec9c0d6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Jan 2023 13:21:58 +0800 Subject: [PATCH 071/734] remove mac sign --- .github/workflows/flutter-ci.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index df47d07d1..738b57c4d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -109,35 +109,6 @@ jobs: - name: Checkout source code uses: actions/checkout@v3 - - name: Import the codesign cert - uses: apple-actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} - p12-password: ${{ secrets.MACOS_P12_PASSWORD }} - keychain: rustdesk - - - name: Check sign and import sign key - run: | - security default-keychain -s rustdesk.keychain - security find-identity -v - - - name: Import notarize key - uses: timheuer/base64-to-file@v1.2 - with: - # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling - fileName: rustdesk.json - fileDir: ${{ github.workspace }} - encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - - - name: Install rcodesign tool - shell: bash - run: | - pushd /tmp - wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz - tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz - mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin - popd - - name: Install build runtime run: | brew install llvm create-dmg nasm yasm cmake gcc wget ninja From 00867276eda18923805ef78e15c341420a3dd04d Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 14:11:49 +0800 Subject: [PATCH 072/734] fix wayland input Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 4 +- .../desktop/screen/desktop_remote_screen.dart | 5 ++- flutter/lib/models/input_model.dart | 26 +++++++++++-- flutter/lib/models/state_model.dart | 1 + libs/enigo/src/linux/nix_impl.rs | 1 + src/common.rs | 6 +-- src/flutter_ffi.rs | 10 ++++- src/keyboard.rs | 38 ++++++++++++++----- src/server/input_service.rs | 2 +- src/ui_session_interface.rs | 3 +- 10 files changed, 75 insertions(+), 21 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 017850cf5..2d0dcacdf 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import '../../models/input_model.dart'; @@ -25,7 +26,8 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: inputModel.handleRawKeyEvent, + onKey: + stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null, child: child)); } } diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index e8361a652..bb6bc431b 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:provider/provider.dart'; /// multi-tab desktop remote screen @@ -10,7 +11,9 @@ class DesktopRemoteScreen extends StatelessWidget { final Map params; DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) { - bind.mainStartGrabKeyboard(); + if (!bind.mainStartGrabKeyboard()) { + stateGlobal.grabKeyboard = true; + } } @override diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 63f86078c..7356c6ec8 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -119,8 +119,11 @@ class InputModel { keyCode = newData.keyCode; } else if (e.data is RawKeyEventDataLinux) { RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; - scanCode = newData.scanCode; - keyCode = newData.keyCode; + // scanCode and keyCode of RawKeyEventDataLinux are incorrect. + // 1. scanCode means keycode + // 2. keyCode means keysym + scanCode = 0; + keyCode = newData.scanCode; } else if (e.data is RawKeyEventDataAndroid) { RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid; scanCode = newData.scanCode + 8; @@ -135,16 +138,33 @@ class InputModel { } else { down = false; } - inputRawKey(e.character ?? "", keyCode, scanCode, down); + inputRawKey(e.character ?? '', keyCode, scanCode, down); } /// Send raw Key Event void inputRawKey(String name, int keyCode, int scanCode, bool down) { + const capslock = 1; + const numlock = 2; + const scrolllock = 3; + int lockModes = 0; + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.capsLock)) { + lockModes |= (1 << capslock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.numLock)) { + lockModes |= (1 << numlock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.scrollLock)) { + lockModes |= (1 << scrolllock); + } bind.sessionHandleFlutterKeyEvent( id: id, name: name, keycode: keyCode, scancode: scanCode, + lockModes: lockModes, downOrUp: down); } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 53f1a19b1..e4c9fa03f 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -9,6 +9,7 @@ import '../consts.dart'; class StateGlobal { int _windowId = -1; bool _fullscreen = false; + bool grabKeyboard = false; final RxBool _showTabBar = true.obs; final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index d9be7b43b..f6e172677 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -183,6 +183,7 @@ impl MouseControllable for Enigo { fn get_led_state(key: Key) -> bool { let led_file = match key { + // FIXME: the file may be /sys/class/leds/input2 or input5 ... Key::CapsLock => "/sys/class/leds/input1::capslock/brightness", Key::NumLock => "/sys/class/leds/input1::numlock/brightness", _ => { diff --git a/src/common.rs b/src/common.rs index 0be84e79f..163937479 100644 --- a/src/common.rs +++ b/src/common.rs @@ -51,7 +51,7 @@ lazy_static::lazy_static! { pub fn global_init() -> bool { #[cfg(target_os = "linux")] { - if !scrap::is_x11() { + if !*IS_X11 { crate::server::wayland::set_wayland_scrap_map_err(); } } @@ -660,13 +660,13 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess #[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { - pub static ref IS_X11: Mutex = Mutex::new(false); + pub static ref IS_X11: bool = false; } #[cfg(target_os = "linux")] lazy_static::lazy_static! { - pub static ref IS_X11: Mutex = Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); + pub static ref IS_X11: bool = "x11" == hbb_common::platform::linux::get_display_server(); } pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 92f1e0606..b30e1e108 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -289,10 +289,11 @@ pub fn session_handle_flutter_key_event( name: String, keycode: i32, scancode: i32, + lock_modes: i32, down_or_up: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.handle_flutter_key_event(&name, keycode, scancode, down_or_up); + session.handle_flutter_key_event(&name, keycode, scancode, lock_modes, down_or_up); } } @@ -1093,8 +1094,13 @@ pub fn main_is_installed() -> SyncReturn { SyncReturn(is_installed()) } -pub fn main_start_grab_keyboard() { +pub fn main_start_grab_keyboard() -> SyncReturn { + #[cfg(target_os = "linux")] + if !*crate::common::IS_X11 { + return SyncReturn(false); + } crate::keyboard::client::start_grab_loop(); + SyncReturn(true) } pub fn main_is_installed_lower_version() -> SyncReturn { diff --git a/src/keyboard.rs b/src/keyboard.rs index a11b0e97e..2d2aada23 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -99,11 +99,11 @@ pub mod client { } } - pub fn process_event(event: &Event) { + pub fn process_event(event: &Event, lock_modes: Option) { if is_long_press(&event) { return; } - if let Some(key_event) = event_to_key_event(&event) { + if let Some(key_event) = event_to_key_event(&event, lock_modes) { send_key_event(&key_event); } } @@ -196,7 +196,7 @@ pub fn start_grab_loop() { return Some(event); } if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(&event); + client::process_event(&event, None); if is_press { return None; } else { @@ -222,7 +222,7 @@ pub fn start_grab_loop() { if let Key::Unknown(keycode) = key { log::error!("rdev get unknown key, keycode is : {:?}", keycode); } else { - client::process_event(&event); + client::process_event(&event, None); } None } @@ -254,7 +254,7 @@ pub fn release_remote_keys() { for key in to_release { let event_type = EventType::KeyRelease(key); let event = event_type_to_event(event_type); - client::process_event(&event); + client::process_event(&event, None); } } @@ -267,7 +267,23 @@ pub fn get_keyboard_mode_enum() -> KeyboardMode { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn add_numlock_capslock_status(key_event: &mut KeyEvent) { +fn add_numlock_capslock_with_lock_modes(key_event: &mut KeyEvent, lock_modes: i32) { + const CAPS_LOCK: i32 = 1; + const NUM_LOCK: i32 = 2; + // const SCROLL_LOCK: i32 = 3; + if lock_modes & (1 << CAPS_LOCK) != 0 { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if lock_modes & (1 << NUM_LOCK) != 0 { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + // if lock_modes & (1 << SCROLL_LOCK) != 0 { + // key_event.modifiers.push(ControlKey::ScrollLock.into()); + // } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn add_numlock_capslock_status(key_event: &mut KeyEvent) { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } @@ -315,7 +331,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_event(event: &Event) -> Option { +pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -345,8 +361,12 @@ pub fn event_to_key_event(event: &Event) -> Option { } } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - add_numlock_capslock_status(&mut key_event); + if let Some(lock_modes) = lock_modes { + add_numlock_capslock_with_lock_modes(&mut key_event, lock_modes); + } else { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + add_numlock_capslock_status(&mut key_event); + } return Some(key_event); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 814fea110..2715a2643 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -882,7 +882,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { // Wayland #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { + if !*IS_X11 { let mut en = ENIGO.lock().unwrap(); let code = evt.chr() as u16; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index c66e1fa3b..7bacb9b21 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -394,6 +394,7 @@ impl Session { name: &str, keycode: i32, scancode: i32, + lock_modes: i32, down_or_up: bool, ) { if scancode < 0 || keycode < 0 { @@ -420,7 +421,7 @@ impl Session { scan_code: scancode as _, event_type: event_type, }; - keyboard::client::process_event(&event); + keyboard::client::process_event(&event, Some(lock_modes)); } // flutter only TODO new input From b0deea57f68a67ecd468f3d8e630808d34f438ef Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Jan 2023 14:41:25 +0800 Subject: [PATCH 073/734] id for cli --- src/cli.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 57d63d397..f8527c99c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -72,6 +72,11 @@ impl Interface for Session { } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { + log::info!( + "id={}, password={}", + crate::ipc::get_id(), + hbb_common::password_security::temporary_password() + ); handle_hash(self.lc.clone(), &pass, hash, self, peer).await; } From 1291c840b9158bc92f25fa0045f7cd2991343e1c Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 14:47:23 +0800 Subject: [PATCH 074/734] fix build Signed-off-by: fufesou --- src/keyboard.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keyboard.rs b/src/keyboard.rs index 2d2aada23..481b6f55c 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -362,6 +362,7 @@ pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option Date: Tue, 10 Jan 2023 14:48:27 +0800 Subject: [PATCH 075/734] trivial change Signed-off-by: fufesou --- src/keyboard.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 481b6f55c..183154ca0 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -361,11 +361,10 @@ pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option Date: Tue, 10 Jan 2023 15:01:46 +0800 Subject: [PATCH 076/734] fix cli --- src/cli.rs | 3 +-- src/main.rs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index f8527c99c..117486ee4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -73,8 +73,7 @@ impl Interface for Session { async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { log::info!( - "id={}, password={}", - crate::ipc::get_id(), + "password={}", hbb_common::password_security::temporary_password() ); handle_hash(self.lc.clone(), &pass, hash, self, peer).await; diff --git a/src/main.rs b/src/main.rs index 67ddb875f..6500a8e4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,6 +91,7 @@ fn main() { let token = LocalConfig::get_option("access_token"); cli::connect_test(p, key, token); } else if let Some(p) = matches.value_of("server") { + log::info!("id={}", hbb_common::config::Config::get_id()); crate::start_server(true); } common::global_clean(); From 4c6145dccf3af06d407fb11c7cdf5f2f5854899c Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 15:12:36 +0800 Subject: [PATCH 077/734] remove unwrap() && fix input source group Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 86707fd62..8dfde0335 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5387,7 +5387,7 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/fufesou/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" +source = "git+https://github.com/fufesou/The-Fat-Controller#a5f13e6ef80327eb8d860aeb26b0af93eb5aee2b" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", From 5d6cb259da89b089e87a28ded917f38b43901577 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 16:07:48 +0800 Subject: [PATCH 078/734] do not show adjust window when scale adaptive Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index b69c73091..db95d33ca 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -956,77 +956,77 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); 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; + 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; + 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; + 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)); } - 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, - ), - ); + }(); + }, + padding: padding, + dismissOnClicked: true, + ), + ); + } } /// Show Codec Preference From a3643f53bf1b80cc2a715f101ce199da2d42c1c1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 17:13:40 +0800 Subject: [PATCH 079/734] set image center when remote resolution is changed Signed-off-by: fufesou --- flutter/lib/common.dart | 6 +++--- flutter/lib/consts.dart | 5 +++++ flutter/lib/desktop/pages/remote_tab_page.dart | 2 +- .../desktop/widgets/kb_layout_type_chooser.dart | 7 ++++--- flutter/lib/desktop/widgets/remote_menubar.dart | 12 ++++++------ flutter/lib/mobile/pages/remote_page.dart | 14 +++++++------- flutter/lib/models/file_model.dart | 3 ++- flutter/lib/models/model.dart | 7 ++++++- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ed78a8e09..93cbe135d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -977,10 +977,10 @@ Future matchPeer(String searchText, Peer peer) async { /// Get the image for the current [platform]. Widget getPlatformImage(String platform, {double size = 50}) { - platform = platform.toLowerCase(); - if (platform == 'mac os') { + if (platform == kPeerPlatformMacOS) { platform = 'mac'; - } else if (platform != 'linux' && platform != 'android') { + } else if (platform != kPeerPlatformLinux && + platform != kPeerPlatformAndroid) { platform = 'win'; } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 7aa200ae9..e4081d9a5 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -5,6 +5,11 @@ import 'package:flutter_hbb/common.dart'; const double kDesktopRemoteTabBarHeight = 28.0; +const String kPeerPlatformWindows = "Windows"; +const String kPeerPlatformLinux = "Linux"; +const String kPeerPlatformMacOS = "Mac OS"; +const String kPeerPlatformAndroid = "Android"; + /// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 604787290..198b2aea7 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -308,7 +308,7 @@ class _ConnectionTabPageState extends State { dismissOnClicked: true, )); - if (pi.platform == 'Linux' || pi.sasEnabled) { + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { menu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( '${translate("Insert")} Ctrl + Alt + Del', diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 58a8f7109..384b0f3bd 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_hbb/models/platform_model.dart'; @@ -170,14 +171,14 @@ RxString KBLayoutType = ''.obs; String getLocalPlatformForKBLayoutType(String peerPlatform) { String localPlatform = ''; - if (peerPlatform != 'Mac OS') { + if (peerPlatform != kPeerPlatformMacOS) { return localPlatform; } if (Platform.isWindows) { - localPlatform = 'Windows'; + localPlatform = kPeerPlatformWindows; } else if (Platform.isLinux) { - localPlatform = 'Linux'; + localPlatform = kPeerPlatformLinux; } // to-do: web desktop support ? return localPlatform; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index db95d33ca..fbeb2d469 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -589,7 +589,7 @@ class _RemoteMenubarState extends State { } displayMenu.add(MenuEntryDivider()); if (perms['keyboard'] != false) { - if (pi.platform == 'Linux' || pi.sasEnabled) { + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( '${translate("Insert")} Ctrl + Alt + Del', @@ -604,9 +604,9 @@ class _RemoteMenubarState extends State { } } if (perms['restart'] != false && - (pi.platform == 'Linux' || - pi.platform == 'Windows' || - pi.platform == 'Mac OS')) { + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS)) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Restart Remote Device'), @@ -633,7 +633,7 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); - if (pi.platform == 'Windows') { + if (pi.platform == kPeerPlatformWindows) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Obx(() => Text( translate( @@ -1157,7 +1157,7 @@ class _RemoteMenubarState extends State { } if (Platform.isWindows && - pi.platform == 'Windows' && + pi.platform == kPeerPlatformWindows && perms['file'] != false) { displayMenu.add(_createSwitchMenuEntry( 'Allow file copy and paste', 'enable-file-transfer', padding, true)); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 97ce6268d..f0c49e9a9 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -574,14 +574,14 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Text(translate('Physical Keyboard Input Mode')), value: 'input-mode')); - if (pi.platform == 'Linux' || pi.sasEnabled) { + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { more.add(PopupMenuItem( child: Text('${translate('Insert')} Ctrl + Alt + Del'), value: 'cad')); } more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); - if (pi.platform == 'Windows' && + if (pi.platform == kPeerPlatformWindows && await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( @@ -591,9 +591,9 @@ class _RemotePageState extends State { } } if (perms["restart"] != false && - (pi.platform == "Linux" || - pi.platform == "Windows" || - pi.platform == "Mac OS")) { + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS)) { more.add(PopupMenuItem( child: Text(translate('Restart Remote Device')), value: 'restart')); } @@ -740,7 +740,7 @@ class _RemotePageState extends State { } final pi = gFFI.ffiModel.pi; - final isMac = pi.platform == "Mac OS"; + final isMac = pi.platform == kPeerPlatformMacOS; final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); @@ -995,7 +995,7 @@ void showOptions( } more.add(getToggle( id, setState, 'lock-after-session-end', 'Lock after session end')); - if (pi.platform == 'Windows') { + if (pi.platform == kPeerPlatformWindows) { more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index c08d2e623..b730e6074 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; @@ -347,7 +348,7 @@ class FileModel extends ChangeNotifier { id: parent.target?.id ?? "", name: "remote_show_hidden")) .isNotEmpty; _remoteOption.isWindows = - parent.target?.ffiModel.pi.platform.toLowerCase() == "windows"; + parent.target?.ffiModel.pi.platform == kPeerPlatformWindows; await Future.delayed(Duration(milliseconds: 100)); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1cdecbd03..3a383d9a1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -61,7 +61,7 @@ class FfiModel with ChangeNotifier { bool get touchMode => _touchMode; - bool get isPeerAndroid => _pi.platform == 'Android'; + bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid; set inputBlocked(v) { _inputBlocked = v; @@ -238,6 +238,11 @@ class FfiModel with ChangeNotifier { if ((_display.width > _display.height) != oldOrientation) { gFFI.canvasModel.updateViewStyle(); } + if (_pi.platform == kPeerPlatformLinux || + _pi.platform == kPeerPlatformWindows || + _pi.platform == kPeerPlatformMacOS) { + parent.target?.canvasModel.updateViewStyle(); + } parent.target?.recordingModel.onSwitchDisplay(); notifyListeners(); } From 8369026c5106821d4432cfa8e7f2ff3d1ff29dca Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Jan 2023 17:30:21 +0800 Subject: [PATCH 080/734] remove mac untested --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index e1dfdbe74..967c85385 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -254,7 +254,7 @@ jobs: - name: Rename rustdesk run: | for name in rustdesk*??.dmg; do - mv "$name" "${name%%.dmg}-untested-${{ matrix.job.target }}.dmg" + mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" done - name: Publish DMG package From aafccf423d07a84c60adc89197ec605395312182 Mon Sep 17 00:00:00 2001 From: ston <2424284164@qq.com> Date: Tue, 10 Jan 2023 18:14:41 +0800 Subject: [PATCH 081/734] fix session type when starting wayland via tty --- libs/hbb_common/src/platform/linux.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index a6ae2a9e7..e82416309 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -74,7 +74,7 @@ fn get_display_server_of_session(session: &str) -> String { } else { "".to_owned() }; - if display_server.is_empty() { + if display_server.is_empty() || display_server == "tty" { // loginctl has not given the expected output. try something else. if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { display_server = sestype; From 5728c69e18ca8b5ed5ef64174f2954de2c984198 Mon Sep 17 00:00:00 2001 From: ilGigioVr88 Date: Tue, 10 Jan 2023 11:37:44 +0100 Subject: [PATCH 082/734] 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 ac3ea46fa..05ee237bd 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -411,7 +411,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local keyboard type", "Tipo di tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), ("software_render_tip", ""), - ("Always use software rendering", ""), + ("Always use software rendering", "Usa sempre il render Software"), ("config_input", ""), ].iter().cloned().collect(); } From 8ab2eddf1748af194f00801719a279f06d7c6c0a Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 11 Jan 2023 10:40:26 +0800 Subject: [PATCH 083/734] opt is_recent_session Signed-off-by: 21pages --- src/server/connection.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index c29faa724..610276f85 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1041,18 +1041,21 @@ impl Connection { false } - fn is_of_recent_session(&mut self) -> bool { + fn is_recent_session(&mut self) -> bool { let session = SESSIONS .lock() .unwrap() .get(&self.lr.my_id) .map(|s| s.to_owned()); + SESSIONS + .lock() + .unwrap() + .retain(|_, s| s.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT); if let Some(session) = session { if session.name == self.lr.my_name && session.session_id == self.lr.session_id && !self.lr.password.is_empty() && self.validate_one_password(session.random_password.clone()) - && session.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT { SESSIONS.lock().unwrap().insert( self.lr.my_id.clone(), @@ -1178,7 +1181,7 @@ impl Connection { { self.send_login_error("Connection not allowed").await; return false; - } else if self.is_of_recent_session() { + } else if self.is_recent_session() { self.try_start_cm(lr.my_id, lr.my_name, true); self.send_logon_response().await; if self.port_forward_socket.is_some() { From 70997acc7f784fcb36967c291ca633f6a3de1f57 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 11 Jan 2023 09:28:51 +0300 Subject: [PATCH 084/734] update ru.rs --- src/lang/ru.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6a9d2f297..43dd1cb08 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Проверить"), + ("Remember me", "Запомнить"), + ("Trust this device", "Доверенное устройство"), + ("Verification code", "Проверочный код"), + ("verification_tip", "Обнаружено новое устройство, на зарегистрированный адрес электронной почты отправлен проверочный код. Введите его, чтобы продолжить вход в систему."), ("Logout", "Выйти"), ("Tags", "Метки"), ("Search ID", "Поиск по ID"), From 3cbcd2e46a654a6c055a30ad2a254eb567661203 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 15:35:35 +0800 Subject: [PATCH 085/734] mac tray icon opt --- Cargo.toml | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 8 +++++++ flutter/pubspec.lock | 4 ++-- res/mac-tray-dark-x2.png | Bin 0 -> 809 bytes res/mac-tray-dark.png | Bin 809 -> 275 bytes res/mac-tray-light-x2.png | Bin 0 -> 810 bytes res/mac-tray-light.png | Bin 810 -> 270 bytes src/platform/macos.mm | 6 +++++ src/platform/macos.rs | 1 - src/tray.rs | 21 +++++++++++++----- 10 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 res/mac-tray-dark-x2.png create mode 100644 res/mac-tray-light-x2.png diff --git a/Cargo.toml b/Cargo.toml index 2713df11d..427fcd4e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,7 +155,7 @@ 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"] +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 a8b5306be..8f11a09ed 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 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,6 +80,8 @@ 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 = ""; }; @@ -131,6 +135,8 @@ 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 */, @@ -259,10 +265,12 @@ 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/flutter/pubspec.lock b/flutter/pubspec.lock index 807f932bb..228817422 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -264,8 +264,8 @@ packages: dependency: "direct main" description: path: "." - ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" - resolved-ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" + ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" + resolved-ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/res/mac-tray-dark-x2.png b/res/mac-tray-dark-x2.png new file mode 100644 index 0000000000000000000000000000000000000000..860f9fcf5f763cec08b68b4d7fc1eb4cf00e2c40 GIT binary patch literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hi;2MYvMt$SQP;xz=dFx){*RWnv{mVKkiQ?V zdVcyc-^*+j$Ib_rS82OFyzp9feZq3v{D;kfhVv2+>fJLr{ag7;#nI{AvrgVvIbFS2 zC$N>J^Ii4ZFM>w=?Cg?T*RVxunlQXQ@TF!#&B9JCAKAoPszr4oCEU?1O)uML`iiUw zPONeEJoW1!3-6u3k+bz@cB>^9Nhm%(>h{7{M)5M2W3iK3^{Hde7EF-HoPBuniapDs zdU9&*Q$5yZ`%ke=vu9-5xmqh*#cARtIgQceD8YNW`njxgN@xNA D=o26P literal 0 HcmV?d00001 diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png index 860f9fcf5f763cec08b68b4d7fc1eb4cf00e2c40..ba8ed8c12cf19a82e8109f4d7b46e10b9afcff99 100644 GIT binary patch literal 275 zcmV+u0qp*XP)^LjVwr$(YZkfw&xwp8a*|yW9xU@X!&-i>hkKYa7Jw~wHWq(XD+h5mh z)W85|71G;cd)RG*1iihAVBP{t!VA6_2xKB~&2%u+g@{~&@v`Z2QavZ4WFe=LJk~`c z_fq^cpOWm-%-*;|XOgj+nVjM+U3CQo&E!FsuD*jQni&ticl8Dw)68M;nhgr+9Z}Em zh^~f5NpmvBfN{>qr`RoEm~RSEG}ppN7JM~KFxuM)Zgn8+x#1w_=D276n`64)?%1ab ZPy``YXG<|C3jzQD002ovPDHLkV1m$=e8m6& literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hi;2MYvMt$SQP;xz=dFx){*RWnv{mVKkiQ?V zdVcyc-^*+j$Ib_rS82OFyzp9feZq3v{D;kfhVv2+>fJLr{ag7;#nI{AvrgVvIbFS2 zC$N>J^Ii4ZFM>w=?Cg?T*RVxunlQXQ@TF!#&B9JCAKAoPszr4oCEU?1O)uML`iiUw zPONeEJoW1!3-6u3k+bz@cB>^9Nhm%(>h{7{M)5M2W3iK3^{Hde7EF-HoPBuniapDs zdU9&*Q$5yZ`%ke=vu9-5xmqh*#cARtIgQceD8YNW`njxgN@xNA D=o26P diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png new file mode 100644 index 0000000000000000000000000000000000000000..f723d980e594a0042a9a94aac6aef9127ec3bf8c GIT binary patch literal 810 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hle!3|jS%9?<#G%X48 z3ua*X{r>rPkH?m}pC4S`E^%>RVX83C>l269ubw}>Eh{EOljH8WiLK=cY6?qh?G5?4 zdUO1`oXWiCi7_xRF?qT;hIkzBopyWLDg&N2XGg)0I$B*y71t!^9==!kKU~_H$8+(8 zr{$rW{+Z;?TEo!qlKIB(e3j^zz3(2fl+F6gc2DxfS02;bfpPlzyc-T&j@J|OY4h7! zV{=?+sj5fGJ?-uM6L(r$oLKR+c*0aCW(}Dcc`TFkO#*LS5nJ{);$44J-?mnb8Fe#m zx-?p4J(l)Ly7pZyN}xgF7Twf^~iz^mV3Ir zv{xNHaqYWv+m&6PTyG|SlRhwgdsSMmi$c(?Rw+G|n|Jq%l`P=NdSU8ymhHg882ts) zSiJo{%)h4Vy?DZl6RZ!KS+!dIXMFC|DX}#CTU5!wJwN7n$#W4Mo{78W8f|0aR@QPj zFkPna$pJw(ec4Q=xSci3+^2&?i>5tGP<&R^8((EHKR&x(^?FsU!=mQEyZ>{-A2jU_ zoOLI9uH7e(#}cVc-YXK@)6#!QVmsiLL6M`kmw-+SEK%^$z{rpQTs1w?%yb@bF(QoD4o*fHCjbk>Dey>_sNgO* z9o~aoQN>>HF*Ja&QN?8NKQw_cQN=`16Mlf5QN>R1DVzh>qlzma88!fpL>b2ct0Tfg z&>@j4F-}q^xEUFS`3dMS*Fv3u&xS>g(OvjN#{d8T literal 810 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hle!3|jS%9?<#G%X48 z3ua*X{r>rPkH?m}pC4S`E^%>RVX83C>l269ubw}>Eh{EOljH8WiLK=cY6?qh?G5?4 zdUO1`oXWiCi7_xRF?qT;hIkzBopyWLDg&N2XGg)0I$B*y71t!^9==!kKU~_H$8+(8 zr{$rW{+Z;?TEo!qlKIB(e3j^zz3(2fl+F6gc2DxfS02;bfpPlzyc-T&j@J|OY4h7! zV{=?+sj5fGJ?-uM6L(r$oLKR+c*0aCW(}Dcc`TFkO#*LS5nJ{);$44J-?mnb8Fe#m zx-?p4J(l)Ly7pZyN}xgF7Twf^~iz^mV3Ir zv{xNHaqYWv+m&6PTyG|SlRhwgdsSMmi$c(?Rw+G|n|Jq%l`P=NdSU8ymhHg882ts) zSiJo{%)h4Vy?DZl6RZ!KS+!dIXMFC|DX}#CTU5!wJwN7n$#W4Mo{78W8f|0aR@QPj zFkPna$pJw(ec4Q=xSci3+^2&?i>5tGP<&R^8((EHKR&x(^?FsU!=mQEyZ>{-A2jU_ zoOLI9uH7e(#}cVc-YXK@)6# bool { } pub fn quit_gui() { - use cocoa::appkit::NSApp; unsafe { let () = msg_send!(NSApp(), terminate: nil); }; diff --git a/src/tray.rs b/src/tray.rs index 98a4127a3..2ee423a9f 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -206,17 +206,28 @@ 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 { + let icon_path = match mode { dark_light::Mode::Dark => { - icon_path = "mac-tray-light.png"; + if f > 1. { + "mac-tray-light_x2.png"; + } else { + "mac-tray-light.png"; + } } dark_light::Mode::Light => { - icon_path = "mac-tray-dark.png"; + if f > 1. { + "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!( "{} {}", From 2ca65a4cf82edd0cf14a8cd2172b76dfeaae06bc Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 15:50:20 +0800 Subject: [PATCH 086/734] fix ci --- src/tray.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 2ee423a9f..0c593c557 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -215,16 +215,16 @@ pub fn make_tray() { let icon_path = match mode { dark_light::Mode::Dark => { if f > 1. { - "mac-tray-light_x2.png"; + "mac-tray-light_x2.png" } else { - "mac-tray-light.png"; + "mac-tray-light.png" } } dark_light::Mode::Light => { if f > 1. { - "mac-tray-dark_x2.png"; + "mac-tray-dark_x2.png" } else { - "mac-tray-dark.png"; + "mac-tray-dark.png" } } }; From 037120fe02ebabe1026da8bc0e42cb3c89ebf2b0 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 15:51:27 +0800 Subject: [PATCH 087/734] typo --- src/tray.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 0c593c557..76dcf3c21 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -215,14 +215,14 @@ pub fn make_tray() { let icon_path = match mode { dark_light::Mode::Dark => { if f > 1. { - "mac-tray-light_x2.png" + "mac-tray-light-x2.png" } else { "mac-tray-light.png" } } dark_light::Mode::Light => { if f > 1. { - "mac-tray-dark_x2.png" + "mac-tray-dark-x2.png" } else { "mac-tray-dark.png" } From 9e9d6fa002c9547b92687de8facad041c63a77a3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 18:41:45 +0800 Subject: [PATCH 088/734] fix linux.svg --- flutter/lib/common.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 93cbe135d..1535c7ad8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -982,6 +982,8 @@ Widget getPlatformImage(String platform, {double size = 50}) { } else if (platform != kPeerPlatformLinux && platform != kPeerPlatformAndroid) { platform = 'win'; + } else { + platform = 'linux'; } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } From 878111f32de47062d9b0ae7031f00292b1e71d6b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 18:49:06 +0800 Subject: [PATCH 089/734] fix a3643f53bf1b80cc2a715f101ce199da2d42c1c1 --- 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 1535c7ad8..9faa06d36 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -983,7 +983,7 @@ Widget getPlatformImage(String platform, {double size = 50}) { platform != kPeerPlatformAndroid) { platform = 'win'; } else { - platform = 'linux'; + platform = platform.toLowerCase(); } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } From 3102a241667c4cf4345f2896b5cb0654550d6b30 Mon Sep 17 00:00:00 2001 From: Asur4s Date: Wed, 11 Jan 2023 23:38:05 +0800 Subject: [PATCH 090/734] fix default keyboard mode when changing version --- src/client.rs | 10 +++++++++- src/common.rs | 7 +++++++ src/ui_session_interface.rs | 7 ++----- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1bb2ff861..fc92c3674 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,6 +7,7 @@ use cpal::{ use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use std::{ + str::FromStr, collections::HashMap, net::SocketAddr, ops::{Deref, Not}, @@ -48,7 +49,7 @@ pub mod file_trait; pub mod helper; pub mod io_loop; use crate::{ - common::is_keyboard_mode_supported, + common::{self, is_keyboard_mode_supported}, server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, }; pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); @@ -1419,6 +1420,13 @@ impl LoginConfigHandler { } else { config.keyboard_mode = KeyboardMode::Legacy.to_string(); } + } else { + let keyboard_modes = + common::get_supported_keyboard_modes(get_version_number(&pi.version)); + let current_mode = &KeyboardMode::from_str(&config.keyboard_mode).unwrap_or_default(); + if !keyboard_modes.contains(current_mode) { + config.keyboard_mode = KeyboardMode::Legacy.to_string(); + } } self.conn_id = pi.conn_id; // no matter if change, for update file time diff --git a/src/common.rs b/src/common.rs index 02b4a0c10..906ea2240 100644 --- a/src/common.rs +++ b/src/common.rs @@ -698,6 +698,13 @@ pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: } } +pub fn get_supported_keyboard_modes(version: i64) -> Vec { + KeyboardMode::iter() + .filter(|&mode| is_keyboard_mode_supported(mode, version)) + .map(|&mode| mode) + .collect::>() +} + #[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { pub static ref IS_X11: Mutex = Mutex::new(false); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index e7ac620ee..33193bd9e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -4,7 +4,7 @@ use crate::client::{ load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -use crate::common::{is_keyboard_mode_supported, GrabState}; +use crate::common::{self, is_keyboard_mode_supported, GrabState}; use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -204,10 +204,7 @@ impl Session { pub fn get_supported_keyboard_modes(&self) -> Vec { let version = self.get_peer_version(); - KeyboardMode::iter() - .filter(|&mode| is_keyboard_mode_supported(mode, version)) - .map(|&mode| mode) - .collect::>() + common::get_supported_keyboard_modes(version) } pub fn remove_port_forward(&self, port: i32) { From 6ad249fa41460ccb5fc0a74d3e1c8a7c284c8261 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 11 Jan 2023 20:27:11 +0100 Subject: [PATCH 091/734] Update de.rs --- src/lang/de.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index a91f167a2..a195fcdba 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Überprüfen"), + ("Remember me", "Login speichern"), + ("Trust this device", "Diesem Gerät vertrauen"), + ("Verification code", "Verifizierungscode"), + ("verification_tip", "Es wurde ein neues Gerät erkannt und ein Verifizierungscode an die registrierte E-Mail-Adresse gesendet. Geben Sie den Verifizierungscode ein, um sich weiter anzumelden."), ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), From 5e2ef998a30ec781831a6105cced70c894d50237 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:19:58 -0700 Subject: [PATCH 092/734] use RS_PUB_KEY env var If RS_PUB_KEY is set as an env variable use the env variable --- libs/hbb_common/src/config.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1d427a2e9..970740045 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -82,7 +82,13 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-sg.rustdesk.com", "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; + +//check for env variable RS_PUB_KEY if not use default +pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { + Some(key) => key, + None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", +}; + pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; @@ -107,7 +113,7 @@ macro_rules! serde_field_string { } #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum NetworkType { +If RS_PUB_KEY is set as an env variable use the env variablepub enum NetworkType { Direct, ProxySocks, } From 860ccd6b3a8cffd9bce8fee153c0c4f273783a37 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:35:47 -0700 Subject: [PATCH 093/734] add env variables for RENDEZVOUS_SERVERS check for env variable RENDEZVOUS_SERVER1-3 if not use the default --- libs/hbb_common/src/config.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 970740045..95b7944fc 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -77,10 +77,20 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; +//check for env variable RENDEZVOUS_SERVER1-3 if not use the default pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ - "rs-ny.rustdesk.com", - "rs-sg.rustdesk.com", - "rs-cn.rustdesk.com", + match option_env!("RENDEZVOUS_SERVER1") { + Some(key) => key, + None => "rs-ny.rustdesk.com", + }, + match option_env!("RENDEZVOUS_SERVER2") { + Some(key) => key, + None => "rs-sg.rustdesk.com", + }, + match option_env!("RENDEZVOUS_SERVER3") { + Some(key) => key, + None => "rs-cn.rustdesk.com", + }, ]; //check for env variable RS_PUB_KEY if not use default From 4c8a3b7adc8a492ad5bf67985986d1975094d0cf Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:41:15 -0700 Subject: [PATCH 094/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 967c85385..775bbd29b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,6 +15,10 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' + RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' + RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' jobs: build-for-windows: From dfc37a0a0b1f579dd25675089bbcedf8b095c203 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:49:17 -0700 Subject: [PATCH 095/734] disable keys on osx if NO_OSX_KEYS is set as a secret do not sign the osx build --- .github/workflows/flutter-nightly.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 775bbd29b..3bf5ebce3 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -19,6 +19,7 @@ env: RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' + NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'False' }} jobs: build-for-windows: @@ -154,6 +155,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert + if: ${{ env.NO_OSX_KEYS!= 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -161,11 +163,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key + if: ${{ env.NO_OSX_KEYS!= 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key + if: ${{ env.NO_OSX_KEYS!= 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -174,6 +178,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool + if: ${{ env.NO_OSX_KEYS!= 'true' }} shell: bash run: | pushd /tmp @@ -244,6 +249,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg + if: ${{ env.NO_OSX_KEYS!= 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain From 61b4ea7b85384d88a80d5370201780907ad865dd Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:53:17 -0700 Subject: [PATCH 096/734] flip boolean --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 3bf5ebce3..556396193 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -19,7 +19,7 @@ env: RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' - NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'False' }} + NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} jobs: build-for-windows: From f21bc352d5138af41f0276d5a8ac857ec8e89d54 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:56:01 -0700 Subject: [PATCH 097/734] test workflow if check --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 556396193..e692497f9 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -155,7 +155,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} From dc9c3ca0082f4278f5ef664a922aaccbd563b529 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:58:20 -0700 Subject: [PATCH 098/734] update all if booleans --- .github/workflows/flutter-nightly.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index e692497f9..370b7e5a5 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -163,13 +163,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -178,7 +178,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} shell: bash run: | pushd /tmp @@ -249,7 +249,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain From ada75b602cc57abc45abd43b123f1134f74a12bd Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 18:29:09 -0700 Subject: [PATCH 099/734] update to RS_PUB_KEY_VAL --- libs/hbb_common/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 95b7944fc..b22ed5e75 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -94,7 +94,7 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ ]; //check for env variable RS_PUB_KEY if not use default -pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { +pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { Some(key) => key, None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", }; From 8dd138c7e2048803e3c43202f790c6d03087e070 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 18:30:08 -0700 Subject: [PATCH 100/734] RS_PUB_KEY_VAL update --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 370b7e5a5..b5ebb1175 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,7 +15,7 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' From 94057d0df5524244f02d5f6464bb2cc73793addd Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 18:59:21 -0700 Subject: [PATCH 101/734] Update config.rs --- libs/hbb_common/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index b22ed5e75..e53bda578 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -123,7 +123,7 @@ macro_rules! serde_field_string { } #[derive(Clone, Copy, PartialEq, Eq, Debug)] -If RS_PUB_KEY is set as an env variable use the env variablepub enum NetworkType { +pub enum NetworkType { Direct, ProxySocks, } From a34781f4bee53d5991c42580e1fdf2d6c6e5f307 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 23:11:52 -0700 Subject: [PATCH 102/734] add NO_APP_KEYS --- .github/workflows/flutter-nightly.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b5ebb1175..f420cce62 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -20,6 +20,7 @@ env: RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} + NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} jobs: build-for-windows: @@ -561,6 +562,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK + if: ${{ env.NO_APP_KEYS== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk From 829fd97e6f13cdbb09821f8c895b3d504f0cb6b6 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 23:32:45 -0700 Subject: [PATCH 103/734] change server check check for custom server by pub_key not for just the option --- src/ui_interface.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9984198b8..f45216d0c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,7 +243,11 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() + if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { + return true + } else { + return false + } } #[inline] From 00dbec3eeee42456175d4f3128f07277a5b62ee8 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 23:58:58 -0700 Subject: [PATCH 104/734] Create flutter-custom-build.yml --- .github/workflows/flutter-custom-build.yml | 1506 ++++++++++++++++++++ 1 file changed, 1506 insertions(+) create mode 100644 .github/workflows/flutter-custom-build.yml diff --git a/.github/workflows/flutter-custom-build.yml b/.github/workflows/flutter-custom-build.yml new file mode 100644 index 000000000..f445ef073 --- /dev/null +++ b/.github/workflows/flutter-custom-build.yml @@ -0,0 +1,1506 @@ +name: Flutter Custom Build + +on: + workflow_dispatch: + +env: + LLVM_VERSION: "10.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" + TAG_NAME: "nightly" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + #To make a custom build with your own servers set the below secret values + RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' + RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' + RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' + RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' + #ignore signing with key files if values below are set + NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} + NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} + +jobs: + build-for-windows: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + - name: Sign rustdesk files + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.CERTNAME }}' + folder: './flutter/build/windows/runner/Release/' + recursive: true + + - name: Build self-extracted executable + shell: bash + run: | + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe + + # - name: Rename rustdesk + # shell: bash + # run: | + # for name in rustdesk*??-install.exe; do + # mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe" + # done + + - name: Sign rustdesk self-extracted file + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' + folder: './SignOutput' + recursive: false + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + build-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-latest, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Import the codesign cert + if: ${{ env.NO_OSX_KEYS== 'true' }} + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + if: ${{ env.NO_OSX_KEYS== 'true' }} + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + if: ${{ env.NO_OSX_KEYS== 'true' }} + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + if: ${{ env.NO_OSX_KEYS== 'true' }} + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm yasm cmake gcc wget ninja + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + shell: bash + run: | + dart pub global activate ffigen --version 5.0.1 + # flutter_rust_bridge + pushd /tmp + wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz + tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz + mkdir -p ~/.cargo/bin + mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen + popd + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk + run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + - name: Codesign app and create signed dmg + if: ${{ env.NO_OSX_KEYS== 'true' }} + run: | + security default-keychain -s rustdesk.keychain + 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 + 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 + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + + - name: Rename rustdesk + run: | + for name in rustdesk*??.dmg; do + mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" + done + + - name: Publish DMG package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk*-${{ matrix.job.target }}.dmg + + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + case "${{ matrix.job.arch }}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + ;; + aarch64|armv7) + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool + esac + cmake --version + gcc -v + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + case "${{ matrix.job.arch }}" in + x86_64) + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + ;; + aarch64|armv7) + pushd /artifacts + # libyuv + git clone https://chromium.googlesource.com/libyuv/libyuv || true + pushd libyuv + git pull + mkdir -p build + pushd build + mkdir -p /artifacts/vcpkg/installed + cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed + make -j4 && make install + popd + popd + # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC + wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz + tar -zxvf opus.tar.gz; ls -l + pushd opus-1.1.2 + ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed + make -j4; make install + ;; + esac + - name: Upload artifacts + uses: actions/upload-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: | + /opt/artifacts/vcpkg/installed + + generate-bridge-linux: + name: generate bridge + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install prerequisites + run: | + sudo apt update -y + sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + workspace: "/tmp/flutter_rust_bridge/frb_codegen" + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v3 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install ffigen + run: | + dart pub global activate ffigen --version 5.0.1 + + - name: Install flutter rust bridge deps + shell: bash + run: | + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + + - name: Run flutter rust bridge + run: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Upload Artifact + uses: actions/upload-artifact@master + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + + build-rustdesk-android-arm64: + needs: [generate-bridge-linux] + name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: x86_64, + target: aarch64-linux-android, + os: ubuntu-18.04, + extra-build-features: "", + } + # - { + # arch: x86_64, + # target: armv7-linux-androideabi, + # os: ubuntu-18.04, + # extra-build-features: "", + # } + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r22b + add-to-path: true + + - name: Download deps + shell: bash + run: | + pushd /opt + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz + tar xzf dep.tar.gz + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + VCPKG_ROOT: /opt/vcpkg + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # download so + pushd flutter + wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzvf so.tar.gz + popd + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + if: ${{ env.NO_APP_KEYS== 'true' }} + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # override default build-tools version (29.0.3) -- optional + BUILD_TOOLS_VERSION: "30.0.2" + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk + path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish apk package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + build-rustdesk-lib-linux-amd64: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + # not ready yet + # distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + 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 + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-lib-linux-arm: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { + # arch: armv7, + # target: arm-unknown-linux-gnueabihf, + # os: ubuntu-20.04, + # use-cross: true, + # extra-build-features: "", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + aarch64) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + ls -l /opt/artifacts/vcpkg/installed/lib/ + mkdir -p /vcpkg/installed/arm64-linux + ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib + 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 + ;; + armv7) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + mkdir -p /vcpkg/installed/arm-linux + ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib + 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 + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-linux-arm: + needs: [build-rustdesk-lib-linux-arm] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 # 20.04 has more performance on arm build + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { + # arch: aarch64, + # target: aarch64-unknown-linux-gnu, + # os: ubuntu-18.04, # just for naming package, not running host + # use-cross: true, + # extra-build-features: "flatpak", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - name: Download Flutter + shell: bash + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /opt + # clone repo and reset to flutter 3.0.5 + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + # reset to flutter 3.0.5 + git fetch + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + popd + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/flutter-elinux:/opt/flutter-elinux" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # we use flutter-elinux to build our rustdesk + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH + flutter-elinux doctor -v + # edit to corresponding arch + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + armv7) + sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py + sed -i "s/x64\/release/arm\/release/g" ./build.py + ;; + esac + python3 ./build.py --flutter --hwcodec --skip-cargo + # rpm package + echo -e "start packaging" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + done + + - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + + - name: Upload Artifact + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/linux\/x64/linux\/arm/g" ./res/PKGBUILD + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD + ;; + esac + + # Temporary disable for there is no many archlinux arm hosts + # - name: Build archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: vufa/arch-makepkg-action@master + # with: + # packages: > + # llvm + # clang + # libva + # libvdpau + # rust + # gstreamer + # unzip + # git + # cmake + # gcc + # curl + # wget + # yasm + # nasm + # zip + # make + # pkg-config + # clang + # gtk3 + # xdotool + # libxcb + # libxfixes + # alsa-lib + # pipewire + # python + # ttf-arphic-uming + # libappindicator-gtk3 + # scripts: | + # cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + # - name: Publish archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # res/rustdesk*.zst + + - name: Publish fedora28/centos8 package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + build-rustdesk-linux-amd64: + needs: [build-rustdesk-lib-linux-amd64] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + ls -l . + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + pushd /workspace + python3 ./build.py --flutter --hwcodec --skip-cargo + # rpm package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + # use cp to duplicate deb files to fit other packages. + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + done + + - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Upload Artifact + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + + - name: Build archlinux package + if: ${{ matrix.job.extra-build-features == '' }} + uses: vufa/arch-makepkg-action@master + with: + packages: > + llvm + clang + libva + libvdpau + rust + gstreamer + unzip + git + cmake + gcc + curl + wget + yasm + nasm + zip + make + pkg-config + clang + gtk3 + xdotool + libxcb + libxfixes + alsa-lib + pipewire + python + ttf-arphic-uming + libappindicator-gtk3 + scripts: | + cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + - name: Publish archlinux package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + res/rustdesk*.zst + + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + + - name: Publish fedora28/centos8 package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + # Temporary disable flatpak arm build + # + # build-flatpak-arm: + # name: Build Flatpak + # needs: [build-rustdesk-linux-arm] + # runs-on: ${{ matrix.job.os }} + # strategy: + # fail-fast: false + # matrix: + # job: + # # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, arch: arm64 } + # - { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, arch: arm64 } + # steps: + # - name: Checkout source code + # uses: actions/checkout@v3 + + # - name: Download Binary + # uses: actions/download-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + # path: . + + # - name: Rename Binary + # run: | + # mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + # - uses: Kingtous/run-on-arch-action@amd64-support + # name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + # id: rpm + # with: + # arch: ${{ matrix.job.arch }} + # distro: ubuntu18.04 + # githubToken: ${{ github.token }} + # setup: | + # ls -l "${PWD}" + # dockerRunArgs: | + # --volume "${PWD}:/workspace" + # shell: /bin/bash + # install: | + # apt update -y + # apt install -y rpm + # run: | + # pushd /workspace + # # install + # apt update -y + # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # # flatpak deps + # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # # package + # pushd flatpak + # git clone https://github.com/flathub/shared-modules.git --depth=1 + # flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + # flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + # - name: Publish flatpak package + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak + + build-flatpak-amd64: + name: Build Flatpak + needs: [build-rustdesk-linux-amd64] + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: . + + - name: Rename Binary + run: | + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + id: rpm + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt update -y + apt install -y rpm git wget curl + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # install + apt update -y + apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # flatpak deps + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # package + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + - name: Publish flatpak package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak From ad44cf0568aebafc5d626afa0d462f694e3a77bf Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 23:59:54 -0700 Subject: [PATCH 105/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f420cce62..2c1f65cff 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,10 +15,12 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + #To make a custom build with your own servers set the below secret values RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' + #ignore signing with key files if values below are set NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} From fee27c5d184c914d1f5ed5892b14c1f84c8206b7 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 12 Jan 2023 00:09:41 -0700 Subject: [PATCH 106/734] set custom-build --- .github/workflows/flutter-custom-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-custom-build.yml b/.github/workflows/flutter-custom-build.yml index f445ef073..8d9518303 100644 --- a/.github/workflows/flutter-custom-build.yml +++ b/.github/workflows/flutter-custom-build.yml @@ -7,7 +7,7 @@ env: LLVM_VERSION: "10.0" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.0.5" - TAG_NAME: "nightly" + TAG_NAME: "custom-build" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" From 0af3dc2ebc8af4bef3af73e8063650f86a3fa66d Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 12 Jan 2023 00:15:06 -0700 Subject: [PATCH 107/734] upload apk if unsigned --- .github/workflows/flutter-custom-build.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-custom-build.yml b/.github/workflows/flutter-custom-build.yml index 8d9518303..7354b6c77 100644 --- a/.github/workflows/flutter-custom-build.yml +++ b/.github/workflows/flutter-custom-build.yml @@ -574,12 +574,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts + if: ${{ env.NO_APP_KEYS== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish apk package + - name: Publish signed apk package + if: ${{ env.NO_APP_KEYS== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -587,6 +589,15 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + - name: Publish unsigned apk package + if: ${{ env.NO_APP_KEYS!= 'true' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] From 01b41f01f65cc33f1440298b75a3e3866fedf10b Mon Sep 17 00:00:00 2001 From: ston Date: Fri, 13 Jan 2023 02:22:57 +0800 Subject: [PATCH 108/734] cn.rs: update wayland_experiment_tip Signed-off-by: ston --- src/lang/cn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a486128b7..4c460a3bd 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -401,7 +401,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_experiment_tip", "Wayland支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), From 7861fab9b8bb1aa61f1bd7da1fffc3f430934705 Mon Sep 17 00:00:00 2001 From: Jimmy GALLAND Date: Thu, 12 Jan 2023 21:09:57 +0100 Subject: [PATCH 109/734] update-fr --- src/lang/fr.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 499be7c54..7c4d55ea3 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Vérifier"), + ("Remember me", "Se souvenir de moi"), + ("Trust this device", "Faire confiance à cet appareil"), + ("Verification code", "Code de vérification"), + ("verification_tip", "Un nouvel appareil a été détecté et un code de vérification a été envoyé à l'adresse e-mail enregistrée, entrez le code de vérification pour continuer la connexion."), ("Logout", "Déconnexion"), ("Tags", "Étiqueter"), ("Search ID", "Rechercher un ID"), @@ -410,8 +410,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Fermé manuellement par la console Web"), ("Local keyboard type", "Disposition du clavier local"), ("Select local keyboard type", "Selectionner la disposition du clavier local"), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), + ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), + ("Always use software rendering", "Utiliser toujours le rendu logiciel"), + ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à Rustdesk l'autorisation \"Surveillance de l’entrée\"."), ].iter().cloned().collect(); } From b4f7fcabadfad3da8c76a8cb354709125190b757 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 13 Jan 2023 00:06:38 -0800 Subject: [PATCH 110/734] opt: make duplicated action panel offstage on macos --- flutter/lib/desktop/widgets/tabbar_widget.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d428bcb9b..8e2238f11 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -374,7 +374,7 @@ class DesktopTab extends StatelessWidget { width: 78, )), Offstage( - offstage: kUseCompatibleUiMode, + offstage: kUseCompatibleUiMode || Platform.isMacOS, child: Row(children: [ Offstage( offstage: !showLogo, @@ -555,7 +555,7 @@ class WindowActionPanelState extends State child: Row( children: [ Offstage( - offstage: !widget.showMinimize, + offstage: !widget.showMinimize || Platform.isMacOS, child: ActionIcon( message: 'Minimize', icon: IconFont.min, @@ -569,7 +569,7 @@ class WindowActionPanelState extends State isClose: false, )), Offstage( - offstage: !widget.showMaximize, + offstage: !widget.showMaximize || Platform.isMacOS, child: Obx(() => ActionIcon( message: widget.isMaximized.value ? "Restore" : "Maximize", @@ -580,7 +580,7 @@ class WindowActionPanelState extends State isClose: false, ))), Offstage( - offstage: !widget.showClose, + offstage: !widget.showClose || Platform.isMacOS, child: ActionIcon( message: 'Close', icon: IconFont.close, From a454aa55cbb66c39ac49787d55e5697db9fd2e52 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:14:17 +0100 Subject: [PATCH 111/734] Update es.rs --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index e0e410711..a956ebca5 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -403,7 +403,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), - ("Skipped", ""), + ("Skipped", "Omitido"), ("Add to Address Book", "Añadir a la libreta de direcciones"), ("Group", "Grupo"), ("Search", "Búsqueda"), From d22e8f4ab8a91753b5424529a8a83994ebe96c8c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 13 Jan 2023 21:09:17 -0700 Subject: [PATCH 112/734] update apk unsigned --- .github/workflows/flutter-nightly.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 2c1f65cff..44804c6ae 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -577,12 +577,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts + if: ${{ env.NO_APP_KEYS== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish apk package + - name: Publish signed apk package + if: ${{ env.NO_APP_KEYS== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -590,6 +592,15 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + - name: Publish unsigned apk package + if: ${{ env.NO_APP_KEYS!= 'true' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] From 95c662f3bcdffc892baaf56ae470a785710eec5f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 14 Jan 2023 14:43:33 +0800 Subject: [PATCH 113/734] fix issue #2819 --- .github/workflows/flutter-nightly.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 967c85385..845ba339b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -245,8 +245,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 - 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 + mv ./flutter/build/macos/Build/Products/Release/rustdesk.app ./flutter/build/macos/Build/Products/Release/RustDesk.app + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v + 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 # notarize the rustdesk-${{ env.VERSION }}.dmg rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg From d3b490ac4834ebe01decefbe7b2c3be9a7e864c8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 12 Jan 2023 21:03:05 +0800 Subject: [PATCH 114/734] elevation request Signed-off-by: 21pages --- flutter/lib/common.dart | 6 +- flutter/lib/mobile/widgets/dialog.dart | 242 +++++++++++++++++++++++++ flutter/lib/models/model.dart | 6 + libs/hbb_common/protos/message.proto | 15 ++ src/client.rs | 2 + src/client/io_loop.rs | 123 ++++++++++--- src/core_main.rs | 3 +- src/flutter_ffi.rs | 12 ++ src/lang/ca.rs | 10 + src/lang/cn.rs | 10 + src/lang/cs.rs | 10 + src/lang/da.rs | 10 + src/lang/de.rs | 10 + src/lang/en.rs | 3 + src/lang/eo.rs | 10 + src/lang/es.rs | 10 + src/lang/fa.rs | 10 + src/lang/fr.rs | 10 + src/lang/gr.rs | 10 + src/lang/hu.rs | 10 + src/lang/id.rs | 10 + src/lang/it.rs | 10 + src/lang/ja.rs | 10 + src/lang/ko.rs | 10 + src/lang/kz.rs | 10 + src/lang/pl.rs | 10 + src/lang/pt_PT.rs | 10 + src/lang/ptbr.rs | 10 + src/lang/ru.rs | 10 + src/lang/sk.rs | 10 + src/lang/sl.rs | 10 + src/lang/sq.rs | 10 + src/lang/sr.rs | 10 + src/lang/sv.rs | 10 + src/lang/template.rs | 10 + src/lang/th.rs | 10 + src/lang/tr.rs | 10 + src/lang/tw.rs | 10 + src/lang/ua.rs | 10 + src/lang/vn.rs | 10 + src/platform/windows.rs | 41 ++++- src/server/connection.rs | 66 ++++++- src/server/portable_service.rs | 100 ++++++++-- src/server/video_service.rs | 16 +- src/ui_cm_interface.rs | 6 +- src/ui_session_interface.rs | 8 + 46 files changed, 900 insertions(+), 59 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9faa06d36..c3a8baba9 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -549,6 +549,10 @@ class OverlayDialogManager { hideMobileActionsOverlay(); } } + + bool existing(String tag) { + return _dialogs.keys.contains(tag); + } } void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { @@ -983,7 +987,7 @@ Widget getPlatformImage(String platform, {double size = 50}) { platform != kPeerPlatformAndroid) { platform = 'win'; } else { - platform = platform.toLowerCase(); + platform = platform.toLowerCase(); } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 2df80d9fd..3b5af1d81 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../common.dart'; import '../../models/model.dart'; @@ -367,6 +368,247 @@ void showServerSettingsWithValue( }); } +void showWaitUacDialog(String id, OverlayDialogManager dialogManager) { + dialogManager.dismissAll(); + dialogManager.show( + tag: '$id-wait-uac', + (setState, close) => CustomAlertDialog( + title: Text(translate('Wait')), + content: Text(translate('wait_accept_uac_tip')).marginAll(10), + )); +} + +void _showRequestElevationDialog( + String id, OverlayDialogManager dialogManager) { + RxString groupValue = ''.obs; + RxString errUser = ''.obs; + RxString errPwd = ''.obs; + TextEditingController userController = TextEditingController(); + TextEditingController pwdController = TextEditingController(); + + void onRadioChanged(String? value) { + if (value != null) { + groupValue.value = value; + } + } + + const minTextStyle = TextStyle(fontSize: 14); + + var content = Obx(() => Column(children: [ + Row( + children: [ + Radio( + value: '', + groupValue: groupValue.value, + onChanged: onRadioChanged), + Expanded( + child: + Text(translate('Ask the remote user for authentication'))), + ], + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate( + 'Choose this if the remote account is administrator'), + style: TextStyle(fontSize: 13)) + .marginOnly(left: 40), + ).marginOnly(bottom: 15), + Row( + children: [ + Radio( + value: 'logon', + groupValue: groupValue.value, + onChanged: onRadioChanged), + Expanded( + child: Text(translate( + 'Transmit the username and password of administrator')), + ) + ], + ), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + '${translate('Username')}:', + style: minTextStyle, + ).marginOnly(right: 10)), + Expanded( + flex: 3, + child: TextField( + controller: userController, + style: minTextStyle, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), + hintText: 'eg: admin', + errorText: errUser.isEmpty ? null : errUser.value), + onChanged: (s) { + if (s.isNotEmpty) { + errUser.value = ''; + } + }, + ), + ) + ], + ).marginOnly(left: 40), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + '${translate('Password')}:', + style: minTextStyle, + ).marginOnly(right: 10)), + Expanded( + flex: 3, + child: TextField( + controller: pwdController, + obscureText: true, + style: minTextStyle, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), + errorText: errPwd.isEmpty ? null : errPwd.value), + onChanged: (s) { + if (s.isNotEmpty) { + errPwd.value = ''; + } + }, + ), + ), + ], + ).marginOnly(left: 40), + Align( + alignment: Alignment.centerLeft, + child: Text(translate('still_click_uac_tip'), + style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)) + .marginOnly(top: 20)), + ])); + + dialogManager.dismissAll(); + dialogManager.show(tag: '$id-request-elevation', (setState, close) { + void submit() { + if (groupValue.value == 'logon') { + if (userController.text.isEmpty) { + errUser.value = translate('Empty Username'); + return; + } + if (pwdController.text.isEmpty) { + errPwd.value = translate('Empty Password'); + return; + } + bind.sessionElevateWithLogon( + id: id, + username: userController.text, + password: pwdController.text); + } else { + bind.sessionElevateDirect(id: id); + } + } + + return CustomAlertDialog( + title: Text(translate('Request Elevation')), + content: content, + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: submit, + child: Text(translate('OK')), + ), + OutlinedButton( + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showOnBlockDialog( + String id, + String type, + String title, + String text, + OverlayDialogManager dialogManager, +) { + if (dialogManager.existing('$id-wait-uac') || + dialogManager.existing('$id-request-elevation')) { + return; + } + var content = Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}", + textAlign: TextAlign.left, + style: TextStyle(fontWeight: FontWeight.w400), + ).marginSymmetric(vertical: 15), + ), + ]); + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + _showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: Text(translate(title)), + content: content, + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: submit, + child: Text(translate('Request Elevation')), + ), + OutlinedButton( + onPressed: () { + close(); + }, + child: Text(translate('Wait')), + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showElevationError(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + _showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: Text(translate(title)), + content: Text(translate(text)), + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: submit, + child: Text(translate('Retry')), + ), + OutlinedButton( + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + Future validateAsync(String value) async { value = value.trim(); if (value.isEmpty) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 3a383d9a1..83678ccb7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -272,6 +272,12 @@ class FfiModel with ChangeNotifier { } else if (type == 'wait-remote-accept-nook') { msgBoxCommon(dialogManager, title, Text(translate(text)), [msgBoxButton("Cancel", closeConnection)]); + } else if (type == 'on-uac' || type == 'on-foreground-elevated') { + showOnBlockDialog(id, type, title, text, dialogManager); + } else if (type == 'wait-uac') { + showWaitUacDialog(id, dialogManager); + } else if (type == 'elevation-error') { + showElevationError(id, type, title, text, dialogManager); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, link, hasRetry, dialogManager); diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index e39bc7c6a..f5910d963 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -552,6 +552,18 @@ message BackNotification { } } +message ElevationRequestWithLogon { + string username = 1; + string password = 2; +} + +message ElevationRequest { + oneof union { + bool direct = 1; + ElevationRequestWithLogon logon = 2; + } +} + message Misc { oneof union { ChatMessage chat_message = 4; @@ -567,6 +579,9 @@ message Misc { bool uac = 15; bool foreground_window_elevated = 16; bool stop_service = 17; + ElevationRequest elevation_request = 18; + string elevation_response = 19; + bool portable_service_running = 20; } } diff --git a/src/client.rs b/src/client.rs index 43ee5bf07..8b2edbcda 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1890,6 +1890,8 @@ pub enum Data { AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), RecordScreen(bool, i32, i32, String), + ElevateDirect, + ElevateWithLogon(String, String), } /// Keycode for key events. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 71d353c88..b15949041 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -632,6 +632,28 @@ impl Remote { .video_sender .send(MediaData::RecordScreen(start, w, h, id)); } + Data::ElevateDirect => { + let mut request = ElevationRequest::new(); + request.set_direct(true); + let mut misc = Misc::new(); + misc.set_elevation_request(request); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + } + Data::ElevateWithLogon(username, password) => { + let mut request = ElevationRequest::new(); + request.set_logon(ElevationRequestWithLogon { + username, + password, + ..Default::default() + }); + let mut misc = Misc::new(); + misc.set_elevation_request(request); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -989,8 +1011,13 @@ impl Remote { self.handler.ui_handler.switch_display(&s); self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { - self.handler - .set_display(s.x, s.y, s.width, s.height, s.cursor_embedded); + self.handler.set_display( + s.x, + s.y, + s.width, + s.height, + s.cursor_embedded, + ); } } Some(misc::Union::CloseReason(c)) => { @@ -1003,31 +1030,85 @@ impl Remote { } } Some(misc::Union::Uac(uac)) => { - let msgtype = "custom-uac-nocancel"; - let title = "Prompt"; - let text = "Please wait for confirmation of UAC..."; - let link = ""; - if uac { - self.handler.msgbox(msgtype, title, text, link); - } else { - self.handler - .cancel_msgbox( - &format!("{}-{}-{}-{}", msgtype, title, text, link,), + #[cfg(feature = "flutter")] + { + if uac { + self.handler.msgbox( + "on-uac", + "Prompt", + "Please wait for confirmation of UAC...", + "", ); + } else { + self.handler.cancel_msgbox("on-uac"); + self.handler.cancel_msgbox("wait-uac"); + self.handler.cancel_msgbox("elevation-error"); + } + } + #[cfg(not(feature = "flutter"))] + { + let msgtype = "custom-uac-nocancel"; + let title = "Prompt"; + let text = "Please wait for confirmation of UAC..."; + let link = ""; + if uac { + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler.cancel_msgbox(&format!( + "{}-{}-{}-{}", + msgtype, title, text, link, + )); + } } } Some(misc::Union::ForegroundWindowElevated(elevated)) => { - let msgtype = "custom-elevated-foreground-nocancel"; - let title = "Prompt"; - let text = "elevated_foreground_window_tip"; - let link = ""; - if elevated { - self.handler.msgbox(msgtype, title, text, link); + #[cfg(feature = "flutter")] + { + if elevated { + self.handler.msgbox( + "on-foreground-elevated", + "Prompt", + "elevated_foreground_window_tip", + "", + ); + } else { + self.handler.cancel_msgbox("on-foreground-elevated"); + self.handler.cancel_msgbox("wait-uac"); + self.handler.cancel_msgbox("elevation-error"); + } + } + #[cfg(not(feature = "flutter"))] + { + let msgtype = "custom-elevated-foreground-nocancel"; + let title = "Prompt"; + let text = "elevated_foreground_window_tip"; + let link = ""; + if elevated { + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler.cancel_msgbox(&format!( + "{}-{}-{}-{}", + msgtype, title, text, link, + )); + } + } + } + Some(misc::Union::ElevationResponse(err)) => { + if err.is_empty() { + self.handler.msgbox("wait-uac", "", "", ""); } else { self.handler - .cancel_msgbox( - &format!("{}-{}-{}-{}", msgtype, title, text, link,), - ); + .msgbox("elevation-error", "Elevation Error", &err, ""); + } + } + Some(misc::Union::PortableServiceRunning(b)) => { + if b { + self.handler.msgbox( + "custom-nocancel", + "Successful", + "Elevate successfully", + "", + ); } } _ => {} diff --git a/src/core_main.rs b/src/core_main.rs index 720c01da8..9083efe0e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -106,7 +106,8 @@ pub fn core_main() -> Option> { && !_is_elevate && !_is_run_as_system { - if let Err(e) = crate::portable_service::client::start_portable_service() { + use crate::portable_service::client; + if let Err(e) = client::start_portable_service(client::StartPara::Direct) { log::error!("Failed to start portable service:{:?}", e); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 594fe7767..a4b6d1395 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -492,6 +492,18 @@ pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) { } } +pub fn session_elevate_direct(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.elevate_direct(); + } +} + +pub fn session_elevate_with_logon(id: String, username: String, password: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.elevate_with_logon(username, password); + } +} + pub fn main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 9224d231a..d54b588c6 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4c460a3bd..61da5d331 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), + ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), + ("Wait","等待"), + ("Elevation Error", "提权失败"), + ("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窗口點擊確認。"), + ("Request Elevation", "请求提权"), + ("wait_accept_uac_tip", "请等待远端用户确认UAC对话框。"), + ("Elevate successfully", "提权成功"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 3622aef8a..d43a534ee 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index f07d9914e..0f7823b72 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a195fcdba..8060deb49 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 14d221ef3..6eed43a77 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -39,5 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."), ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), + ("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.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 2a41fdcf9..8503d153a 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index e0e410711..b695a6802 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -413,5 +413,15 @@ 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\"."), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 790d01682..6bdd05061 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 7c4d55ea3..bbc50e4a6 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), ("Always use software rendering", "Utiliser toujours le rendu logiciel"), ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à Rustdesk l'autorisation \"Surveillance de l’entrée\"."), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 53369a4b3..7a0e0b35d 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 32d920994..342a29bc1 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index c33cccb66..671c2a8fc 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 05ee237bd..cd132dc45 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", "Usa sempre il render Software"), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 7dd1640f6..4343d5cec 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 66ff3ca95..c9874b2d6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index ac688eb9f..049d490a1 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index afd6b4b03..5d0d575b9 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index bf7954b46..0749678d8 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 207be548f..f6c43aab0 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 43dd1cb08..77f64e75d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 40f19c625..954e3ae92 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 5e8efc17d..3b4ee16da 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 0725d02e5..804a88798 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 3b7201bb8..9ecdd1843 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index eeeec80cc..ccf1eed8a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index d3be7ba17..f8a7a3bc7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a4d0a033d..5bd969bdc 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 2d0fc8c59..ee661d9b6 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index a58665a70..aebf8917e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), ("config_input", ""), + ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), + ("Wait","等待"), + ("Elevation Error", "提權失敗"), + ("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", "依然需要被控端用戶在UAC窗口點擊確認。"), + ("Request Elevation", "請求提權"), + ("wait_accept_uac_tip", "請等待遠端用戶確認UAC對話框。"), + ("Elevate successfully", "提權成功"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index fad7a3880..784592ff0 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 187572c83..ac62631c9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a2a99800f..89861a418 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -24,7 +24,7 @@ use winapi::{ minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, - OpenProcessToken, + OpenProcessToken, PROCESS_INFORMATION, STARTUPINFOW, }, securitybaseapi::GetTokenInformation, shellapi::ShellExecuteW, @@ -1714,3 +1714,42 @@ pub fn send_message_to_hnwd( } return true; } + +pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { + unsafe { + let wuser = wide_string(user); + let wpc = wide_string(""); + let wpwd = wide_string(pwd); + let cmd = if arg.is_empty() { + format!("\"{}\"", exe) + } else { + format!("\"{}\" {}", exe, arg) + }; + let mut wcmd = wide_string(&cmd); + let mut si: STARTUPINFOW = mem::zeroed(); + si.wShowWindow = SW_HIDE as _; + si.lpDesktop = NULL as _; + si.cb = std::mem::size_of::() as _; + si.dwFlags = STARTF_USESHOWWINDOW; + let mut pi: PROCESS_INFORMATION = mem::zeroed(); + let wexe = wide_string(exe); + if FALSE + == CreateProcessWithLogonW( + wuser.as_ptr(), + wpc.as_ptr(), + wpwd.as_ptr(), + LOGON_WITH_PROFILE, + wexe.as_ptr(), + wcmd.as_mut_ptr(), + CREATE_UNICODE_ENVIRONMENT, + NULL, + NULL as _, + &mut si as *mut STARTUPINFOW, + &mut pi as *mut PROCESS_INFORMATION, + ) + { + bail!("CreateProcessWithLogonW failed, errno={}", GetLastError()); + } + } + return Ok(()); +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 93e90395e..a7526c8b4 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -101,6 +101,7 @@ pub struct Connection { last_recv_time: Arc>, chat_unanswered: bool, close_manually: bool, + elevation_requested: bool, } impl Subscriber for ConnInner { @@ -196,6 +197,7 @@ impl Connection { last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, close_manually: false, + elevation_requested: false, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -247,6 +249,8 @@ impl Connection { #[cfg(windows)] let mut last_foreground_window_elevated = false; #[cfg(windows)] + let mut last_portable_service_running = false; + #[cfg(windows)] let is_installed = crate::platform::is_installed(); loop { @@ -353,7 +357,8 @@ impl Connection { } #[cfg(windows)] ipc::Data::DataPortableService(ipc::DataPortableService::RequestStart) => { - if let Err(e) = crate::portable_service::client::start_portable_service() { + use crate::portable_service::client; + if let Err(e) = client::start_portable_service(client::StartPara::Direct) { log::error!("Failed to start portable service from cm:{:?}", e); } } @@ -440,8 +445,18 @@ impl Connection { _ = second_timer.tick() => { #[cfg(windows)] { - if !is_installed { - let portable_service_running = crate::portable_service::client::PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); + if !is_installed && conn.file_transfer.is_none() && conn.port_forward_socket.is_none(){ + let portable_service_running = crate::portable_service::client::running(); + if portable_service_running != last_portable_service_running { + last_portable_service_running = portable_service_running; + if portable_service_running && conn.elevation_requested { + let mut misc = Misc::new(); + misc.set_portable_service_running(portable_service_running); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.inner.send(msg.into()); + } + } let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); if last_uac != uac { last_uac = uac; @@ -1476,6 +1491,51 @@ impl Connection { } } } + Some(misc::Union::ElevationRequest(r)) => match r.union { + Some(elevation_request::Union::Direct(_)) => { + #[cfg(windows)] + { + let mut err = "No need to elevate".to_string(); + if !crate::platform::is_installed() + && !crate::portable_service::client::running() + { + use crate::portable_service::client; + err = client::start_portable_service(client::StartPara::Direct) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + self.elevation_requested = err.is_empty(); + let mut misc = Misc::new(); + misc.set_elevation_response(err); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(msg).await; + } + } + Some(elevation_request::Union::Logon(r)) => { + #[cfg(windows)] + { + let mut err = "No need to elevate".to_string(); + if !crate::platform::is_installed() + && !crate::portable_service::client::running() + { + use crate::portable_service::client; + err = client::start_portable_service(client::StartPara::Logon( + r.username, r.password, + )) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + self.elevation_requested = err.is_empty(); + let mut misc = Misc::new(); + misc.set_elevation_response(err); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(msg).await; + } + } + _ => {} + }, _ => {} }, _ => {} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 6b87da21b..0651fd4ce 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -451,18 +451,24 @@ pub mod server { // functions called in main process. pub mod client { use hbb_common::anyhow::Context; + use std::path::PathBuf; use super::*; lazy_static::lazy_static! { - pub static ref PORTABLE_SERVICE_RUNNING: Arc> = Default::default(); + static ref RUNNING: Arc> = Default::default(); static ref SHMEM: Arc>> = Default::default(); static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); } - pub(crate) fn start_portable_service() -> ResultType<()> { + pub enum StartPara { + Direct, + Logon(String, String), + } + + pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> { log::info!("start portable service"); - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { bail!("already running"); } if SHMEM.lock().unwrap().is_none() { @@ -491,14 +497,60 @@ pub mod client { unsafe { libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } - if crate::platform::run_background( - &std::env::current_exe()?.to_string_lossy().to_string(), - "--portable-service", - ) - .is_err() - { - *SHMEM.lock().unwrap() = None; - bail!("Failed to run portable service process"); + drop(option); + match para { + StartPara::Direct => { + if let Err(e) = crate::platform::run_background( + &std::env::current_exe()?.to_string_lossy().to_string(), + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process:{}", e); + } + } + StartPara::Logon(username, password) => { + #[allow(unused_mut)] + let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); + #[cfg(feature = "flutter")] + { + if let Some(dir) = PathBuf::from(&exe).parent() { + if !set_dir_permission(&PathBuf::from(dir)) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to set permission of {:?}", dir); + } + } + } + #[cfg(not(feature = "flutter"))] + match hbb_common::directories_next::UserDirs::new() { + Some(user_dir) => { + let dir = user_dir + .home_dir() + .join("AppData") + .join("Local") + .join("rustdesk-sciter"); + if std::fs::create_dir_all(&dir).is_ok() { + let dst = dir.join("rustdesk.exe"); + if std::fs::copy(&exe, &dst).is_ok() { + if dst.exists() { + if set_dir_permission(&dir) { + exe = dst.to_string_lossy().to_string(); + } + } + } + } + } + None => {} + } + if let Err(e) = crate::platform::windows::create_process_with_logon( + username.as_str(), + password.as_str(), + &exe, + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process:{}", e); + } + } } let _sender = SENDER.lock().unwrap(); Ok(()) @@ -509,6 +561,16 @@ pub mod client { *SHMEM.lock().unwrap() = None; } + fn set_dir_permission(dir: &PathBuf) -> bool { + // // give Everyone RX permission + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg("Everyone:(OI)(CI)RX") + .arg("/T") + .spawn() + .is_ok() + } pub struct CapturerPortable; impl CapturerPortable { @@ -668,7 +730,7 @@ pub mod client { } Pong => { nack = 0; - *PORTABLE_SERVICE_RUNNING.lock().unwrap() = true; + *RUNNING.lock().unwrap() = true; }, ConnCount(None) => { if !quick_support { @@ -699,7 +761,7 @@ pub mod client { } } } - *PORTABLE_SERVICE_RUNNING.lock().unwrap() = false; + *RUNNING.lock().unwrap() = false; }); } Err(err) => { @@ -752,7 +814,7 @@ pub mod client { use_yuv: bool, portable_service_running: bool, ) -> ResultType> { - if portable_service_running != PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if portable_service_running != RUNNING.lock().unwrap().clone() { log::info!("portable service status mismatch"); } if portable_service_running { @@ -767,7 +829,7 @@ pub mod client { } pub fn get_cursor_info(pci: PCURSORINFO) -> BOOL { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { get_cursor_info_(&mut SHMEM.lock().unwrap().as_mut().unwrap(), pci) } else { unsafe { winuser::GetCursorInfo(pci) } @@ -775,7 +837,7 @@ pub mod client { } pub fn handle_mouse(evt: &MouseEvent) { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { handle_mouse_(evt).ok(); } else { crate::input_service::handle_mouse_(evt); @@ -783,12 +845,16 @@ pub mod client { } pub fn handle_key(evt: &KeyEvent) { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { handle_key_(evt).ok(); } else { crate::input_service::handle_key_(evt); } } + + pub fn running() -> bool { + RUNNING.lock().unwrap().clone() + } } #[repr(C)] diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 618b003e9..599dfbd54 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -20,14 +20,10 @@ use super::{video_qos::VideoQoS, *}; #[cfg(windows)] -use crate::portable_service::client::PORTABLE_SERVICE_RUNNING; -#[cfg(windows)] use hbb_common::get_version_number; -use hbb_common::{ - tokio::sync::{ - mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Mutex as TokioMutex, - }, +use hbb_common::tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, }; #[cfg(not(windows))] use scrap::Capturer; @@ -419,7 +415,7 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; #[cfg(windows)] - let last_portable_service_running = PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); + let last_portable_service_running = crate::portable_service::client::running(); #[cfg(not(windows))] let last_portable_service_running = false; @@ -518,14 +514,14 @@ fn run(sp: GenericService) -> ResultType<()> { bail!("SWITCH"); } #[cfg(windows)] - if last_portable_service_running != PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if last_portable_service_running != crate::portable_service::client::running() { bail!("SWITCH"); } check_privacy_mode_changed(&sp, c.privacy_mode_id)?; #[cfg(windows)] { if crate::platform::windows::desktop_changed() - && !PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() + && !crate::portable_service::client::running() { bail!("Desktop changed"); } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index a32662d07..551352ff7 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -780,11 +780,7 @@ fn cm_inner_send(id: i32, data: Data) { pub fn can_elevate() -> bool { #[cfg(windows)] { - return !crate::platform::is_installed() - && !crate::portable_service::client::PORTABLE_SERVICE_RUNNING - .lock() - .unwrap() - .clone(); + return !crate::platform::is_installed() && !crate::portable_service::client::running(); } #[cfg(not(windows))] return false; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 46bf39b78..00f1f90cf 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -608,6 +608,14 @@ impl Session { } self.update_transfer_list(); } + + pub fn elevate_direct(&self) { + self.send(Data::ElevateDirect); + } + + pub fn elevate_with_logon(&self, username: String, password: String) { + self.send(Data::ElevateWithLogon(username, password)); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From 62791613a76ac05e3c045e6797f63fd0e2f33b73 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 15 Jan 2023 19:46:16 +0800 Subject: [PATCH 115/734] opt dialog button style Signed-off-by: 21pages --- flutter/lib/common.dart | 36 ++++++- flutter/lib/common/widgets/address_book.dart | 8 +- flutter/lib/common/widgets/dialog.dart | 96 +++++++++--------- flutter/lib/common/widgets/login.dart | 6 +- flutter/lib/common/widgets/peer_card.dart | 12 +-- .../lib/desktop/pages/desktop_home_page.dart | 4 +- .../desktop/pages/desktop_setting_page.dart | 4 +- .../lib/desktop/pages/file_manager_page.dart | 11 +-- .../widgets/kb_layout_type_chooser.dart | 2 +- .../lib/desktop/widgets/remote_menubar.dart | 14 +-- .../lib/desktop/widgets/tabbar_widget.dart | 4 +- .../lib/mobile/pages/file_manager_page.dart | 27 +++-- flutter/lib/mobile/pages/remote_page.dart | 13 +-- flutter/lib/mobile/pages/settings_page.dart | 13 ++- flutter/lib/mobile/widgets/dialog.dart | 99 ++++++------------- flutter/lib/models/file_model.dart | 25 +---- flutter/lib/models/model.dart | 2 +- flutter/lib/models/server_model.dart | 13 ++- 18 files changed, 166 insertions(+), 223 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c3a8baba9..6ffa5ccb2 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -668,24 +668,25 @@ void msgBox(String id, String type, String title, String text, String link, if (type != "connecting" && type != "success" && !type.contains("nook")) { hasOk = true; - buttons.insert(0, msgBoxButton(translate('OK'), submit)); + buttons.insert(0, dialogButton('OK', onPressed: submit)); } hasCancel ??= !type.contains("error") && !type.contains("nocancel") && type != "restarting"; if (hasCancel) { - buttons.insert(0, msgBoxButton(translate('Cancel'), cancel)); + buttons.insert( + 0, dialogButton('Cancel', onPressed: cancel, isOutline: true)); } // TODO: test this button if (type.contains("hasclose")) { buttons.insert( 0, - msgBoxButton(translate('Close'), () { + dialogButton('Close', onPressed: () { dialogManager.dismissAll(); })); } if (link.isNotEmpty) { - buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink)); + buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); } dialogManager.show( (setState, close) => CustomAlertDialog( @@ -1566,3 +1567,30 @@ class ServerConfig { apiServer = options['api-server'] ?? "", key = options['key'] ?? ""; } + +Widget dialogButton(String text, + {required VoidCallback? onPressed, + bool isOutline = false, + TextStyle? style}) { + if (isDesktop) { + if (isOutline) { + return OutlinedButton( + onPressed: onPressed, + child: Text(translate(text), style: style), + ); + } else { + return ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: onPressed, + child: Text(translate(text), style: style), + ); + } + } else { + return TextButton( + onPressed: onPressed, + child: Text( + translate(text), + style: style, + )); + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 34d5af485..5c1e1218c 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -335,8 +335,8 @@ class _AddressBookState extends State { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -402,8 +402,8 @@ class _AddressBookState extends State { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index a6de0384f..837a197dc 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -64,8 +64,8 @@ void changeIdDialog() { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -111,48 +111,46 @@ void changeWhiteList({Function()? callback}) async { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetOption(key: 'whitelist', value: ''); - callback?.call(); - close(); - }, - child: Text(translate("Clear"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - }); - newWhiteListField = controller.text.trim(); - var newWhiteList = ""; - if (newWhiteListField.isEmpty) { - // pass - } else { - final ips = - newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); - // test ip - final ipMatch = RegExp( - r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$"); - final ipv6Match = RegExp( - r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$"); - for (final ip in ips) { - if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) { - msg = "${translate("Invalid IP")} $ip"; - setState(() { - isInProgress = false; - }); - return; - } + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Clear", onPressed: () async { + await bind.mainSetOption(key: 'whitelist', value: ''); + callback?.call(); + close(); + }, isOutline: true), + dialogButton( + "OK", + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + newWhiteListField = controller.text.trim(); + var newWhiteList = ""; + if (newWhiteListField.isEmpty) { + // pass + } else { + final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + // test ip + final ipMatch = RegExp( + r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$"); + final ipv6Match = RegExp( + r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$"); + for (final ip in ips) { + if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) { + msg = "${translate("Invalid IP")} $ip"; + setState(() { + isInProgress = false; + }); + return; } - newWhiteList = ips.join(','); } - await bind.mainSetOption(key: 'whitelist', value: newWhiteList); - callback?.call(); - close(); - }, - child: Text(translate("OK"))), + newWhiteList = ips.join(','); + } + await bind.mainSetOption(key: 'whitelist', value: newWhiteList); + callback?.call(); + close(); + }, + ), ], onCancel: close, ); @@ -195,14 +193,12 @@ Future changeDirectAccessPort( ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetOption( - key: 'direct-access-port', value: controller.text); - close(); - }, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: () async { + await bind.mainSetOption( + key: 'direct-access-port', value: controller.text); + close(); + }), ], onCancel: close, ); diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 15105ae61..2f10ac005 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -550,7 +550,7 @@ Future loginDialog() async { ), ], ), - actions: [msgBoxButton(translate('Close'), onDialogCancel)], + actions: [dialogButton('Close', onPressed: onDialogCancel)], onCancel: onDialogCancel, ); }); @@ -667,8 +667,8 @@ Future verificationCodeDialog(UserPayload? user) async { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: onVerify, child: Text(translate("Verify"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Verify", onPressed: onVerify), ]); }); diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index a98739606..c07b458bc 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -662,8 +662,8 @@ abstract class BasePeerCard extends StatelessWidget { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -931,8 +931,8 @@ class AddressBookPeerCard extends BasePeerCard { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -1095,8 +1095,8 @@ void _rdpDialog(String id, CardType card) async { ), ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index fd9814cc2..471a84b1d 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -627,8 +627,8 @@ void setPasswordDialog() async { ), ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9f2dc988e..df87a0ead 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1671,8 +1671,8 @@ void changeSocks5Proxy() async { ), ), actions: [ - TextButton(onPressed: close, child: Text(translate('Cancel'))), - TextButton(onPressed: submit, child: Text(translate('OK'))), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 60b22a516..b6a9e5fed 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -802,14 +802,9 @@ class _FileManagerPageState extends State ], ), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))) + dialogButton("Cancel", + onPressed: cancel, isOutline: true), + dialogButton("OK", onPressed: submit) ], onSubmit: submit, onCancel: cancel, diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 384b0f3bd..90e72cd40 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -218,7 +218,7 @@ showKBLayoutTypeChooser( KBLayoutType.value = bind.getLocalKbLayoutType(); return v == KBLayoutType.value; }), - actions: [msgBoxButton(translate('Close'), close)], + actions: [dialogButton('Close', onPressed: close)], onCancel: close, ); }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6545f8556..6a0fa9104 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -809,7 +809,7 @@ class _RemoteMenubarState extends State { } if (newValue == kRemoteImageQualityCustom) { - final btnClose = msgBoxButton(translate('Close'), () async { + final btnClose = dialogButton('Close', onPressed: () async { await setCustomValues(); widget.ffi.dialogManager.dismissAll(); }); @@ -1326,16 +1326,8 @@ void showSetOSPassword( ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: close, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 8e2238f11..ec494cf22 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -687,8 +687,8 @@ Future closeConfirmDialog() async { ]), // confirm checkbox actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 549a44b78..7aa9a0005 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -174,23 +174,18 @@ class _FileManagerPageState extends State { ], ), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton("Cancel", onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir(PathUtil.join( - model.currentDir.path, - name.value.text, - model - .getCurrentIsWindows())); - close(); - } - }, - child: Text(translate("OK"))) + isOutline: true), + dialogButton("OK", onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir(PathUtil.join( + model.currentDir.path, + name.value.text, + model.getCurrentIsWindows())); + close(); + } + }) ])); } else if (v == "hidden") { model.toggleShowHidden(); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index f0c49e9a9..0a10d8011 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1098,15 +1098,9 @@ void showSetOSPassword( ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton( + 'OK', onPressed: () { var text = controller.text.trim(); bind.sessionPeerOption(id: id, name: "os-password", value: text); @@ -1117,7 +1111,6 @@ void showSetOSPassword( } close(); }, - child: Text(translate('OK')), ), ]); }); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index b14f3ee65..c5f3b6935 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -273,13 +273,12 @@ class _SettingsState extends State with WidgetsBindingObserver { content: Text(translate( "android_open_battery_optimizations_tip")), actions: [ - TextButton( - onPressed: () => close(), - child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), - child: - Text(translate("Open System Setting"))), + dialogButton("Cancel", + onPressed: () => close(), isOutline: true), + dialogButton( + "Open System Setting", + onPressed: () => close(true), + ), ], )); if (res == true) { diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 3b5af1d81..0eb403833 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/widgets/button.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -33,10 +34,8 @@ void showRestartRemoteDevice( content: Text( "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), actions: [ - TextButton( - onPressed: () => close(), child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: () => close(), isOutline: true), + dialogButton("OK", onPressed: () => close(true)), ], )); if (res == true) bind.sessionRestartRemoteDevice(id: id); @@ -96,15 +95,15 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ), ])), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton( + 'Cancel', onPressed: () { close(); }, - child: Text(translate('Cancel')), + isOutline: true, ), - TextButton( - style: flatButtonStyle, + dialogButton( + 'OK', onPressed: (validateLength && validateSame) ? () async { close(); @@ -118,7 +117,6 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { } } : null, - child: Text(translate('OK')), ), ], ); @@ -198,16 +196,8 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: cancel, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -220,20 +210,19 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { title: Text(translate('Wrong Password')), content: Text(translate('Do you want to enter again?')), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton( + 'Cancel', onPressed: () { close(); closeConnection(); }, - child: Text(translate('Cancel')), + isOutline: true, ), - TextButton( - style: flatButtonStyle, + dialogButton( + 'Retry', onPressed: () { enterPasswordDialog(id, dialogManager); }, - child: Text(translate('Retry')), ), ])); } @@ -321,15 +310,11 @@ void showServerSettingsWithValue( child: LinearProgressIndicator()) ])), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton( + 'OK', onPressed: () async { setState(() { idServerMsg = null; @@ -361,7 +346,6 @@ void showServerSettingsWithValue( isInProgress = false; }); }, - child: Text(translate('OK')), ), ], ); @@ -512,17 +496,8 @@ void _showRequestElevationDialog( title: Text(translate('Request Elevation')), content: content, actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0), - onPressed: submit, - child: Text(translate('OK')), - ), - OutlinedButton( - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -561,17 +536,10 @@ void showOnBlockDialog( title: Text(translate(title)), content: content, actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0), - onPressed: submit, - child: Text(translate('Request Elevation')), - ), - OutlinedButton( - onPressed: () { - close(); - }, - child: Text(translate('Wait')), - ), + dialogButton('Wait', onPressed: () { + close(); + }, isOutline: true), + dialogButton('Request Elevation', onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -591,17 +559,10 @@ void showElevationError(String id, String type, String title, String text, title: Text(translate(title)), content: Text(translate(text)), actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0), - onPressed: submit, - child: Text(translate('Retry')), - ), - OutlinedButton( - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton('Retry', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index b730e6074..18d42d143 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -665,14 +665,8 @@ class FileModel extends ChangeNotifier { : const SizedBox.shrink() ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: cancel, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -724,18 +718,9 @@ class FileModel extends ChangeNotifier { : const SizedBox.shrink() ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(null), - child: Text(translate("Skip"))), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: cancel, isOutline: true), + dialogButton("Skip", onPressed: () => close(null), isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: cancel, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 83678ccb7..641165e67 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -271,7 +271,7 @@ class FfiModel with ChangeNotifier { hasCancel: false); } else if (type == 'wait-remote-accept-nook') { msgBoxCommon(dialogManager, title, Text(translate(text)), - [msgBoxButton("Cancel", closeConnection)]); + [dialogButton("Cancel", onPressed: closeConnection)]); } else if (type == 'on-uac' || type == 'on-foreground-elevated') { showOnBlockDialog(id, type, title, text, dialogManager); } else if (type == 'wait-uac') { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 338da4ee3..c36a54db6 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -304,8 +304,8 @@ class ServerModel with ChangeNotifier { ]), content: Text(translate("android_service_will_start_tip")), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -501,8 +501,8 @@ class ServerModel with ChangeNotifier { ], ), actions: [ - TextButton(onPressed: cancel, child: Text(translate("Dismiss"))), - ElevatedButton(onPressed: submit, child: Text(translate("Accept"))), + dialogButton("Dismiss", onPressed: cancel, isOutline: true), + dialogButton("Accept", onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -674,9 +674,8 @@ showInputWarnAlert(FFI ffi) { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: submit, child: Text(translate("Open System Setting"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Open System Setting", onPressed: submit), ], onSubmit: submit, onCancel: close, From 6a46783ebff24ad6603cec2c826ce218275571f3 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Sun, 15 Jan 2023 15:30:44 +0100 Subject: [PATCH 116/734] Update it.rs --- src/lang/it.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index cd132dc45..858edbd84 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -410,18 +410,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), ("Local keyboard type", "Tipo di tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), - ("software_render_tip", ""), + ("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", ""), - ("request_elevation_tip", ""), - ("Wait",""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), + ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), + ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), + ("Wait", "Attendi"), + ("Elevation Error", "Errore durante l'elevazione dei diritti"), + ("Ask the remote user for authentication", "Chiedere l'autenticazione all'utente remoto"), + ("Choose this if the remote account is administrator", "Scegliere questa opzione se l'account remoto è amministratore"), + ("Transmit the username and password of administrator", "Trasmettere il nome utente e la password dell'amministratore"), + ("still_click_uac_tip", "Richiede ancora che l'utente remoto faccia clic su OK nella finestra UAC dell'esecuzione di RustDesk."), + ("Request Elevation", "Richiedi elevazione dei diritti"), + ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), + ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), ].iter().cloned().collect(); } From ea3e0fd906c25c6068150bf14c042d2f065a12bc Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 15 Jan 2023 18:12:47 +0100 Subject: [PATCH 117/734] Update de.rs --- src/lang/de.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 8060deb49..74c674b56 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -26,7 +26,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Keyboard/Mouse", "Tastatur und Maus aktivieren"), ("Enable Clipboard", "Zwischenablage aktivieren"), ("Enable File Transfer", "Dateiübertragung aktivieren"), - ("Enable TCP Tunneling", "TCP-Tunnel aktivieren"), + ("Enable TCP Tunneling", "TCP-Tunnelung aktivieren"), ("IP Whitelisting", "IP-Whitelist"), ("ID/Relay Server", "ID/Vermittlungsserver"), ("Import Server Config", "Serverkonfiguration importieren"), @@ -39,7 +39,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ändern"), ("Website", "Webseite"), ("About", "Über"), - ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt"), + ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), ("Audio Input", "Audioeingang"), @@ -224,7 +224,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add Tag", "Stichwort hinzufügen"), ("Unselect all tags", "Alle Stichworte abwählen"), ("Network error", "Netzwerkfehler"), - ("Username missed", "Benutzername vergessen"), + ("Username missed", "Benutzernamen vergessen"), ("Password missed", "Passwort vergessen"), ("Wrong credentials", "Falsche Anmeldedaten"), ("Edit Tag", "Schlagwort bearbeiten"), @@ -413,15 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), - ("request_elevation_tip", ""), - ("Wait",""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), + ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), + ("Wait","Warten"), + ("Elevation Error", "Berechtigungsfehler"), + ("Ask the remote user for authentication", "Den entfernten Benutzer zur Authentifizierung auffordern"), + ("Choose this if the remote account is administrator", "Wählen Sie dies, wenn das entfernte Konto Administrator ist"), + ("Transmit the username and password of administrator", "Übermitteln Sie den Benutzernamen und das Passwort des Administrators"), + ("still_click_uac_tip", "Der entfernte Benutzer muss immer noch im UAC-Fenster von RustDesk auf OK klicken."), + ("Request Elevation", "Erhöhte Rechte anfordern"), + ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), + ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), ].iter().cloned().collect(); } From 485479c31b42798ff32f8dda0cb2d771827714a0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 14 Jan 2023 15:09:25 +0800 Subject: [PATCH 118/734] sync: depend on web Signed-off-by: 21pages --- src/hbbs_http/sync.rs | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index 9497cc449..a060d6a20 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -54,6 +54,7 @@ async fn start_hbbs_sync_async() { last_send = Instant::now(); let mut v = Value::default(); v["id"] = json!(Config::get_id()); + v["ver"] = json!(hbb_common::get_version_number(crate::VERSION)); if !conns.is_empty() { v["conns"] = json!(conns); } @@ -100,33 +101,16 @@ fn heartbeat_url() -> String { } fn handle_config_options(config_options: HashMap) { - let map = HashMap::from([ - ("enable-keyboard", ""), - ("enable-clipboard", ""), - ("enable-file-transfer", ""), - ("enable-audio", ""), - ("enable-tunnel", ""), - ("enable-remote-restart", ""), - ("enable-record-session", ""), - ("allow-remote-config-modification", ""), - ("approve-mode", ""), - ("verification-method", "use-both-passwords"), - ("enable-rdp", ""), - ("enable-lan-discovery", ""), - ("direct-server", ""), - ("direct-access-port", ""), - ]); let mut options = Config::get_options(); - for (k, v) in map { - if let Some(v2) = config_options.get(k) { - if v == v2 { + config_options + .iter() + .map(|(k, v)| { + if v.is_empty() { options.remove(k); } else { - options.insert(k.to_string(), v2.to_string()); + options.insert(k.to_string(), v.to_string()); } - } else { - options.remove(k); - } - } + }) + .count(); Config::set_options(options); } From 9aecd287028e92f73d861002b5abcb88ca6be85e Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 16 Jan 2023 19:47:58 +0800 Subject: [PATCH 119/734] complex pernament password lowercase, uppercase, digit, length>=8 Signed-off-by: 21pages --- .../lib/common/widgets/custom_password.dart | 121 ++++++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 103 ++++++++++----- flutter/pubspec.yaml | 1 + src/lang/ca.rs | 10 +- src/lang/cn.rs | 10 +- src/lang/cs.rs | 10 +- src/lang/da.rs | 10 +- src/lang/de.rs | 10 +- src/lang/eo.rs | 10 +- src/lang/es.rs | 10 +- src/lang/fa.rs | 10 +- src/lang/fr.rs | 10 +- src/lang/gr.rs | 10 +- src/lang/hu.rs | 10 +- src/lang/id.rs | 10 +- src/lang/it.rs | 8 ++ src/lang/ja.rs | 10 +- src/lang/ko.rs | 10 +- src/lang/kz.rs | 10 +- src/lang/pl.rs | 10 +- src/lang/pt_PT.rs | 10 +- src/lang/ptbr.rs | 10 +- src/lang/ru.rs | 10 +- src/lang/sk.rs | 10 +- src/lang/sl.rs | 10 +- src/lang/sq.rs | 10 +- src/lang/sr.rs | 10 +- src/lang/sv.rs | 10 +- src/lang/template.rs | 10 +- src/lang/th.rs | 10 +- src/lang/tr.rs | 10 +- src/lang/tw.rs | 10 +- src/lang/ua.rs | 10 +- src/lang/vn.rs | 10 +- 34 files changed, 468 insertions(+), 65 deletions(-) create mode 100644 flutter/lib/common/widgets/custom_password.dart diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart new file mode 100644 index 000000000..99ece2434 --- /dev/null +++ b/flutter/lib/common/widgets/custom_password.dart @@ -0,0 +1,121 @@ +// https://github.com/rodrigobastosv/fancy_password_field +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:get/get.dart'; +import 'package:password_strength/password_strength.dart'; + +abstract class ValidationRule { + String get name; + bool validate(String value); +} + +class UppercaseValidationRule extends ValidationRule { + @override + String get name => translate('uppercase'); + @override + bool validate(String value) { + return value.contains(RegExp(r'[A-Z]')); + } +} + +class LowercaseValidationRule extends ValidationRule { + @override + String get name => translate('lowercase'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[a-z]')); + } +} + +class DigitValidationRule extends ValidationRule { + @override + String get name => translate('digit'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[0-9]')); + } +} + +class SpecialCharacterValidationRule extends ValidationRule { + @override + String get name => translate('special character'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]')); + } +} + +class MinCharactersValidationRule extends ValidationRule { + final int _numberOfCharacters; + MinCharactersValidationRule(this._numberOfCharacters); + + @override + String get name => translate('length>=$_numberOfCharacters'); + + @override + bool validate(String value) { + return value.length >= _numberOfCharacters; + } +} + +class PasswordStrengthIndicator extends StatelessWidget { + final RxString password; + final double weakMedium = 0.33; + final double mediumStrong = 0.67; + const PasswordStrengthIndicator({Key? key, required this.password}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + var strength = estimatePasswordStrength(password.value); + return Row( + children: [ + Expanded( + child: _indicator( + password.isEmpty ? Colors.grey : _getColor(strength))), + Expanded( + child: _indicator(password.isEmpty || strength < weakMedium + ? Colors.grey + : _getColor(strength))), + Expanded( + child: _indicator(password.isEmpty || strength < mediumStrong + ? Colors.grey + : _getColor(strength))), + Text(password.isEmpty ? '' : translate(_getLabel(strength))) + .marginOnly(left: password.isEmpty ? 0 : 8), + ], + ); + }); + } + + Widget _indicator(Color color) { + return Container( + height: 8, + color: color, + ); + } + + String _getLabel(double strength) { + if (strength < weakMedium) { + return 'Weak'; + } else if (strength < mediumStrong) { + return 'Medium'; + } else { + return 'Strong'; + } + } + + Color _getColor(double strength) { + if (strength < weakMedium) { + return Colors.yellow; + } else if (strength < mediumStrong) { + return Colors.blue; + } else { + return Colors.green; + } + } +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 471a84b1d..65c38e06b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -6,6 +6,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/custom_password.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; @@ -543,6 +544,14 @@ void setPasswordDialog() async { final p1 = TextEditingController(text: pw); var errMsg0 = ""; var errMsg1 = ""; + final RxString rxPass = p0.text.obs; + final rules = [ + DigitValidationRule(), + UppercaseValidationRule(), + LowercaseValidationRule(), + // SpecialCharacterValidationRule(), + MinCharactersValidationRule(8), + ]; gFFI.dialogManager.show((setState, close) { submit() { @@ -551,15 +560,20 @@ void setPasswordDialog() async { errMsg1 = ""; }); final pass = p0.text.trim(); - if (pass.length < 6 && pass.isNotEmpty) { - setState(() { - errMsg0 = translate("Too short, at least 6 characters."); - }); - return; + if (pass.isNotEmpty) { + for (var r in rules) { + if (!r.validate(pass)) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${r.name}'; + }); + return; + } + } } if (p1.text.trim() != pass) { setState(() { - errMsg1 = translate("The confirmation is not identical."); + errMsg1 = + '${translate('Prompt')}: ${translate("The confirmation is not identical.")}'; }); return; } @@ -579,23 +593,44 @@ void setPasswordDialog() async { ), Row( children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - "${translate('Password')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(15), + labelText: translate('Password'), border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, focusNode: FocusNode()..requestFocus(), + onChanged: (value) { + rxPass.value = value; + }, + ), + ), + ], + ), + Row( + children: [ + Expanded(child: PasswordStrengthIndicator(password: rxPass)), + ], + ).marginSymmetric(vertical: 8), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(15), + border: const OutlineInputBorder(), + labelText: translate('Confirmation'), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, ), ), ], @@ -603,26 +638,24 @@ void setPasswordDialog() async { const SizedBox( height: 8.0, ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Confirmation')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: errMsg1.isNotEmpty ? errMsg1 : null), - controller: p1, - ), - ), - ], - ), + Obx(() => Wrap( + runSpacing: 8, + spacing: 4, + children: rules.map((e) { + var checked = e.validate(rxPass.value.trim()); + 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(), + )) ], ), ), diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 705f4650c..f096218b0 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -93,6 +93,7 @@ dependencies: auto_size_text: ^3.0.0 bot_toast: ^4.0.3 win32: any + password_strength: ^0.2.0 dev_dependencies: diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d54b588c6..bbcea1347 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 61da5d331..2f56b6da0 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), - ("Wait","等待"), + ("Wait", "等待"), ("Elevation Error", "提权失败"), ("Ask the remote user for authentication", "请求远端用户授权"), ("Choose this if the remote account is administrator", "当对面电脑是管理员账号时选择该选项"), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "请求提权"), ("wait_accept_uac_tip", "请等待远端用户确认UAC对话框。"), ("Elevate successfully", "提权成功"), + ("uppercase", "大写字母"), + ("lowercase", "小写字母"), + ("digit", "数字"), + ("special character", "特殊字符"), + ("length>=8", "长度不小于8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "强"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d43a534ee..8852d602c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 0f7823b72..53ae46bd4 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 74c674b56..292b2ed28 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "Software-Rendering immer verwenden"), ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), - ("Wait","Warten"), + ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), ("Ask the remote user for authentication", "Den entfernten Benutzer zur Authentifizierung auffordern"), ("Choose this if the remote account is administrator", "Wählen Sie dies, wenn das entfernte Konto Administrator ist"), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Erhöhte Rechte anfordern"), ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 8503d153a..955a3287d 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index c06d3f775..bae1b5cbf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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\"."), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 6bdd05061..a257425f1 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index bbc50e4a6..6edec8477 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "Utiliser toujours le rendu logiciel"), ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à Rustdesk l'autorisation \"Surveillance de l’entrée\"."), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 7a0e0b35d..81a50bcd8 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 342a29bc1..9e1a4d982 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 671c2a8fc..65c30ec6d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 858edbd84..f94669d34 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Richiedi elevazione dei diritti"), ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 4343d5cec..6ebb11ef5 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index c9874b2d6..a6825b523 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 049d490a1..816eb370d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5d0d575b9..df985cccf 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0749678d8..dba37b5da 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f6c43aab0..31c9153f2 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 77f64e75d..b22d49cc2 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 954e3ae92..56d14652d 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 3b4ee16da..3d2ad3be8 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 804a88798..165597e7e 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 9ecdd1843..739d53570 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ccf1eed8a..498131d0c 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f8a7a3bc7..adb05c943 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 5bd969bdc..2b062c3f7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index ee661d9b6..a4a179c86 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index aebf8917e..cd9f270ec 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "使用軟件渲染"), ("config_input", ""), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), - ("Wait","等待"), + ("Wait", "等待"), ("Elevation Error", "提權失敗"), ("Ask the remote user for authentication", "請求遠端用戶授權"), ("Choose this if the remote account is administrator", "當對面電腦是管理員賬號時選擇該選項"), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "請求提權"), ("wait_accept_uac_tip", "請等待遠端用戶確認UAC對話框。"), ("Elevate successfully", "提權成功"), + ("uppercase", "大寫字母"), + ("lowercase", "小寫字母"), + ("digit", "數字"), + ("special character", "特殊字符"), + ("length>=8", "長度不小於8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "強"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 784592ff0..ff24baab7 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index ac62631c9..6988efba7 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } From cc0f4509a7685ff1e03f0c2b33e049616dacfaa6 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 16 Jan 2023 20:24:21 +0800 Subject: [PATCH 120/734] common dialog InputDecoration Signed-off-by: 21pages --- flutter/lib/common.dart | 10 ++++++++-- flutter/lib/desktop/pages/desktop_home_page.dart | 4 ---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6ffa5ccb2..04d7b85d0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -634,8 +634,14 @@ class CustomAlertDialog extends StatelessWidget { title: title, contentPadding: EdgeInsets.symmetric( horizontal: contentPadding ?? 25, vertical: 10), - content: - ConstrainedBox(constraints: contentBoxConstraints, child: content), + content: ConstrainedBox( + constraints: contentBoxConstraints, + child: Theme( + data: ThemeData( + inputDecorationTheme: InputDecorationTheme( + isDense: true, contentPadding: EdgeInsets.all(15)), + ), + child: content)), actions: actions, ), ); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 65c38e06b..2773a3049 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -597,8 +597,6 @@ void setPasswordDialog() async { child: TextField( obscureText: true, decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.all(15), labelText: translate('Password'), border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), @@ -625,8 +623,6 @@ void setPasswordDialog() async { child: TextField( obscureText: true, decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.all(15), border: const OutlineInputBorder(), labelText: translate('Confirmation'), errorText: errMsg1.isNotEmpty ? errMsg1 : null), From d793fa64a3eb0c6bbf95ca08ffa169522cf39920 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 16 Jan 2023 20:58:42 +0800 Subject: [PATCH 121/734] dialog tab order Signed-off-by: 21pages --- flutter/lib/common.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 04d7b85d0..23aa9535d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -613,8 +613,9 @@ class CustomAlertDialog extends StatelessWidget { Future.delayed(Duration.zero, () { if (!focusNode.hasFocus) focusNode.requestFocus(); }); - return Focus( - focusNode: focusNode, + FocusScopeNode scopeNode = FocusScopeNode(); + return FocusScope( + node: scopeNode, autofocus: true, onKey: (node, key) { if (key.logicalKey == LogicalKeyboardKey.escape) { @@ -626,6 +627,11 @@ class CustomAlertDialog extends StatelessWidget { 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(); + } + return KeyEventResult.handled; } return KeyEventResult.ignored; }, From 260b8f0b12727536e323da5ec405f2e70ed2e668 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:51:30 +0100 Subject: [PATCH 122/734] Update de.rs --- src/lang/de.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 292b2ed28..dd05dcdd5 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Vermittlungsdienst aktiv"), ("Service is not running", "Vermittlungsdienst deaktiviert"), ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung."), - ("Control Remote Desktop", "Entfernten PC steuern"), + ("Control Remote Desktop", "Entfernten Desktop steuern"), ("Transfer File", "Datei übertragen"), ("Connect", "Verbinden"), ("Recent Sessions", "Letzte Sitzungen"), @@ -28,7 +28,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable File Transfer", "Dateiübertragung aktivieren"), ("Enable TCP Tunneling", "TCP-Tunnelung aktivieren"), ("IP Whitelisting", "IP-Whitelist"), - ("ID/Relay Server", "ID/Vermittlungsserver"), + ("ID/Relay Server", "ID/Relay-Server"), ("Import Server Config", "Serverkonfiguration importieren"), ("Export Server Config", "Serverkonfiguration exportieren"), ("Import server configuration successfully", "Serverkonfiguration erfolgreich importiert"), @@ -47,7 +47,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hardware Codec", "Hardware-Codec"), ("Adaptive Bitrate", "Bitrate automatisch anpassen"), ("ID Server", "ID-Server"), - ("Relay Server", "Vermittlungsserver"), + ("Relay Server", "Relay-Server"), ("API Server", "API-Server"), ("invalid_http", "Muss mit http:// oder https:// beginnen"), ("Invalid IP", "Ungültige IP-Adresse"), @@ -127,15 +127,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), ("ID does not exist", "Diese ID existiert nicht."), - ("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), + ("Failed to connect to rendezvous server", "Verbindung zum Rendezvous-Server fehlgeschlagen"), ("Please try later", "Bitte versuchen Sie es später erneut."), - ("Remote desktop is offline", "Entfernter PC ist offline."), + ("Remote desktop is offline", "Entfernter Desktop ist offline."), ("Key mismatch", "Schlüssel stimmen nicht überein."), ("Timeout", "Zeitüberschreitung"), - ("Failed to connect to relay server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), - ("Failed to connect via rendezvous server", "Verbindung über Vermittlungsserver ist fehlgeschlagen"), + ("Failed to connect to relay server", "Verbindung zum Relay-Server ist fehlgeschlagen"), + ("Failed to connect via rendezvous server", "Verbindung über Rendezvous-Server ist fehlgeschlagen"), ("Failed to connect via relay server", "Verbindung über Relay-Server ist fehlgeschlagen"), - ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"), + ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten Desktop ist fehlgeschlagen"), ("Set Password", "Passwort festlegen"), ("OS Password", "Betriebssystem-Passwort"), ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten und installieren RustDesk auf dem System."), @@ -423,13 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Erhöhte Rechte anfordern"), ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("uppercase", "Großbuchstaben"), + ("lowercase", "Kleinbuchstaben"), + ("digit", "Ziffern"), + ("special character", "Sonderzeichen"), + ("length>=8", "Länge ≥ 8"), + ("Weak", "Schwach"), + ("Medium", "Mittel"), + ("Strong", "Stark"), ].iter().cloned().collect(); } From edb6e307ec017ada94cf0a820d593f64783ad7d8 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 13:26:42 -0700 Subject: [PATCH 123/734] add spaces --- .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 b7cb1bb32..d27b151b3 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,12 +15,12 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - #To make a custom build with your own servers set the below secret values + # To make a custom build with your own servers set the below secret values RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' - #ignore signing with key files if values below are set + # Ignore signing with key files if values below are set NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} From 0173f79ecf05e9d6cb8e0cdcdd7a64ff9f384d4e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 13:30:54 -0700 Subject: [PATCH 124/734] Test original check --- src/ui_interface.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index f45216d0c..9984198b8 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,11 +243,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { - return true - } else { - return false - } + crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } #[inline] From c157a5b1304c87c9d33f2f0894cdd7c3549cc96e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 13:45:43 -0700 Subject: [PATCH 125/734] Change from NO_XYZ_KEYS to SKIP_ --- .github/workflows/flutter-nightly.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index d27b151b3..bf092e5a7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -21,8 +21,8 @@ env: RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' # Ignore signing with key files if values below are set - NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} - NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} + SKIP_OSX_KEYS: ${{ secrets.SKIP_OSX_KEYS == 'true' }} + SKIP_APP_KEYS: ${{ secrets.SKIP_APP_KEYS == 'true' }} jobs: build-for-windows: @@ -158,7 +158,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -166,13 +166,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -181,7 +181,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} shell: bash run: | pushd /tmp @@ -252,7 +252,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -565,7 +565,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.NO_APP_KEYS== 'true' }} + if: ${{ env.SKIP_APP_KEYS== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -578,14 +578,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.NO_APP_KEYS== 'true' }} + if: ${{ env.SKIP_APP_KEYS== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ env.NO_APP_KEYS== 'true' }} + if: ${{ env.SKIP_APP_KEYS== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -594,7 +594,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ env.NO_APP_KEYS!= 'true' }} + if: ${{ env.SKIP_APP_KEYS!= 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 233305ee0181ee843d3ca54ae4ac9a08cd26785e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 13:52:33 -0700 Subject: [PATCH 126/734] used the key files to check --- .github/workflows/flutter-nightly.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index bf092e5a7..112cb53ad 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -20,9 +20,6 @@ env: RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' - # Ignore signing with key files if values below are set - SKIP_OSX_KEYS: ${{ secrets.SKIP_OSX_KEYS == 'true' }} - SKIP_APP_KEYS: ${{ secrets.SKIP_APP_KEYS == 'true' }} jobs: build-for-windows: @@ -158,7 +155,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -166,13 +163,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -181,7 +178,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -252,7 +249,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -565,7 +562,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.SKIP_APP_KEYS== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -578,14 +575,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.SKIP_APP_KEYS== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ env.SKIP_APP_KEYS== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -594,7 +591,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ env.SKIP_APP_KEYS!= 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 9cf81d20320139cb418f0c46c5fa010cf9fb0b15 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 14:10:04 -0700 Subject: [PATCH 127/734] updated ui to check key value --- src/ui_interface.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9984198b8..5b5d3c218 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,7 +243,12 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { + if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() + return true + } else { + return false + } } #[inline] From 3cd1d42aa25157f2b8d65066607432c1a62c0c0e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 14:16:35 -0700 Subject: [PATCH 128/734] remove old line --- src/ui_interface.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 5b5d3c218..f45216d0c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -244,7 +244,6 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { - crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() return true } else { return false From 42d5ca2419ac441c74c68a9ed890964c9fd34532 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:56:43 +0800 Subject: [PATCH 129/734] Delete flutter-custom-build.yml I dislike duplication. --- .github/workflows/flutter-custom-build.yml | 1517 -------------------- 1 file changed, 1517 deletions(-) delete mode 100644 .github/workflows/flutter-custom-build.yml diff --git a/.github/workflows/flutter-custom-build.yml b/.github/workflows/flutter-custom-build.yml deleted file mode 100644 index 7354b6c77..000000000 --- a/.github/workflows/flutter-custom-build.yml +++ /dev/null @@ -1,1517 +0,0 @@ -name: Flutter Custom Build - -on: - workflow_dispatch: - -env: - LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" - TAG_NAME: "custom-build" - # vcpkg version: 2022.05.10 - # for multiarch gcc compatibility - VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" - VERSION: "1.2.0" - #To make a custom build with your own servers set the below secret values - RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' - RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' - RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' - RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' - #ignore signing with key files if values below are set - NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} - NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} - -jobs: - build-for-windows: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: x86_64-pc-windows-msvc, os: windows-2019 } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install LLVM and Clang - uses: KyleMayes/install-llvm-action@v1 - with: - version: ${{ env.LLVM_VERSION }} - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - name: Replace engine with rustdesk custom flutter engine - run: | - flutter doctor -v - flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: "1.62" - target: ${{ matrix.job.target }} - override: true - components: rustfmt - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.job.os }} - - - name: Install flutter rust bridge deps - run: | - dart pub global activate ffigen --version 5.0.1 - $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe - Push-Location .. - git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 - Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location - Pop-Location - Push-Location flutter ; flutter pub get ; Pop-Location - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 - with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - shell: bash - - - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec --flutter - - - name: Sign rustdesk files - uses: GermanBluefox/code-sign-action@v7 - with: - certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' - password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' - certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' - # certificatename: '${{ secrets.CERTNAME }}' - folder: './flutter/build/windows/runner/Release/' - recursive: true - - - name: Build self-extracted executable - shell: bash - run: | - pushd ./libs/portable - python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe - popd - mkdir -p ./SignOutput - mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe - - # - name: Rename rustdesk - # shell: bash - # run: | - # for name in rustdesk*??-install.exe; do - # mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe" - # done - - - name: Sign rustdesk self-extracted file - uses: GermanBluefox/code-sign-action@v7 - with: - certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' - password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' - certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' - # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' - folder: './SignOutput' - recursive: false - - - name: Publish Release - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./SignOutput/rustdesk-*.exe - - build-for-macOS: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - target: x86_64-apple-darwin, - os: macos-latest, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Import the codesign cert - if: ${{ env.NO_OSX_KEYS== 'true' }} - uses: apple-actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} - p12-password: ${{ secrets.MACOS_P12_PASSWORD }} - keychain: rustdesk - - - name: Check sign and import sign key - if: ${{ env.NO_OSX_KEYS== 'true' }} - run: | - security default-keychain -s rustdesk.keychain - security find-identity -v - - - name: Import notarize key - if: ${{ env.NO_OSX_KEYS== 'true' }} - uses: timheuer/base64-to-file@v1.2 - with: - # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling - fileName: rustdesk.json - fileDir: ${{ github.workspace }} - encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - - - name: Install rcodesign tool - if: ${{ env.NO_OSX_KEYS== 'true' }} - shell: bash - run: | - pushd /tmp - wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz - tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz - mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin - popd - - - name: Install build runtime - run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.job.os }} - - - name: Install flutter rust bridge deps - shell: bash - run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp - wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz - tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz - mkdir -p ~/.cargo/bin - mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen - popd - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 - with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install libvpx libyuv opus - - - name: Show version information (Rust, cargo, Clang) - shell: bash - run: | - clang --version || true - rustup -V - rustup toolchain list - rustup default - cargo -V - rustc -V - - - name: Build rustdesk - run: | - # --hwcodec not supported on macos yet - ./build.py --flutter ${{ matrix.job.extra-build-args }} - - - name: Codesign app and create signed dmg - if: ${{ env.NO_OSX_KEYS== 'true' }} - run: | - security default-keychain -s rustdesk.keychain - 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 - 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 - # notarize the rustdesk-${{ env.VERSION }}.dmg - rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg - - - name: Rename rustdesk - run: | - for name in rustdesk*??.dmg; do - mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" - done - - - name: Publish DMG package - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk*-${{ matrix.job.target }}.dmg - - build-vcpkg-deps-linux: - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - # - { arch: armv7, os: ubuntu-20.04 } - - { arch: x86_64, os: ubuntu-20.04 } - - { arch: aarch64, os: ubuntu-20.04 } - steps: - - name: Create vcpkg artifacts folder - run: mkdir -p /opt/artifacts - - - name: Cache Vcpkg - id: cache-vcpkg - uses: actions/cache@v3 - with: - path: /opt/artifacts - key: vcpkg-${{ matrix.job.arch }} - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Run vcpkg install on ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "/opt/artifacts" - dockerRunArgs: | - --volume "/opt/artifacts:/artifacts" - shell: /bin/bash - install: | - apt update -y - case "${{ matrix.job.arch }}" in - x86_64) - # CMake 3.15+ - apt install -y gpg wget ca-certificates - echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - apt update -y - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev - ;; - aarch64|armv7) - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool - esac - cmake --version - gcc -v - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - case "${{ matrix.job.arch }}" in - x86_64) - export VCPKG_FORCE_SYSTEM_BINARIES=1 - pushd /artifacts - git clone https://github.com/microsoft/vcpkg.git || true - pushd vcpkg - git reset --hard ${{ env.VCPKG_COMMIT_ID }} - ./bootstrap-vcpkg.sh - ./vcpkg install libvpx libyuv opus - ;; - aarch64|armv7) - pushd /artifacts - # libyuv - git clone https://chromium.googlesource.com/libyuv/libyuv || true - pushd libyuv - git pull - mkdir -p build - pushd build - mkdir -p /artifacts/vcpkg/installed - cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed - make -j4 && make install - popd - popd - # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC - wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz - tar -zxvf opus.tar.gz; ls -l - pushd opus-1.1.2 - ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed - make -j4; make install - ;; - esac - - name: Upload artifacts - uses: actions/upload-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: | - /opt/artifacts/vcpkg/installed - - generate-bridge-linux: - name: generate bridge - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install prerequisites - run: | - sudo apt update -y - sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: bridge-${{ matrix.job.os }} - workspace: "/tmp/flutter_rust_bridge/frb_codegen" - - - name: Cache Bridge - id: cache-bridge - uses: actions/cache@v3 - with: - path: /tmp/flutter_rust_bridge - key: vcpkg-${{ matrix.job.arch }} - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - name: Install ffigen - run: | - dart pub global activate ffigen --version 5.0.1 - - - name: Install flutter rust bridge deps - shell: bash - run: | - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd - pushd flutter && flutter pub get && popd - - - name: Run flutter rust bridge - run: | - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Upload Artifact - uses: actions/upload-artifact@master - with: - name: bridge-artifact - path: | - ./src/bridge_generated.rs - ./flutter/lib/generated_bridge.dart - ./flutter/lib/generated_bridge.freezed.dart - - build-rustdesk-android-arm64: - needs: [generate-bridge-linux] - name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - arch: x86_64, - target: aarch64-linux-android, - os: ubuntu-18.04, - extra-build-features: "", - } - # - { - # arch: x86_64, - # target: armv7-linux-androideabi, - # os: ubuntu-18.04, - # extra-build-features: "", - # } - steps: - - name: Install dependencies - run: | - sudo apt update - sudo apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless - - name: Checkout source code - uses: actions/checkout@v3 - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: r22b - add-to-path: true - - - name: Download deps - shell: bash - run: | - pushd /opt - wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz - tar xzf dep.tar.gz - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - - - name: Build rustdesk lib - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} - VCPKG_ROOT: /opt/vcpkg - run: | - rustup target add ${{ matrix.job.target }} - cargo install cargo-ndk - case ${{ matrix.job.target }} in - aarch64-linux-android) - ./flutter/ndk_arm64.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - ;; - armv7-linux-androideabi) - ./flutter/ndk_arm.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so - ;; - esac - - - name: Build rustdesk - shell: bash - env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 - run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH - # download so - pushd flutter - wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz - tar xzvf so.tar.gz - popd - # temporary use debug sign config - sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle - case ${{ matrix.job.target }} in - aarch64-linux-android) - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - # build flutter - pushd flutter - flutter build apk --release --target-platform android-arm64 --split-per-abi - mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - ;; - armv7-linux-androideabi) - mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so - # build flutter - pushd flutter - flutter build apk --release --target-platform android-arm --split-per-abi - mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - ;; - esac - popd - mkdir -p signed-apk; pushd signed-apk - mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . - - - uses: r0adkll/sign-android-release@v1 - name: Sign app APK - if: ${{ env.NO_APP_KEYS== 'true' }} - id: sign-rustdesk - with: - releaseDirectory: ./signed-apk - signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} - alias: ${{ secrets.ANDROID_ALIAS }} - keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} - keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} - env: - # override default build-tools version (29.0.3) -- optional - BUILD_TOOLS_VERSION: "30.0.2" - - - name: Upload Artifacts - if: ${{ env.NO_APP_KEYS== 'true' }} - uses: actions/upload-artifact@master - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk - path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - - name: Publish signed apk package - if: ${{ env.NO_APP_KEYS== 'true' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - - name: Publish unsigned apk package - if: ${{ env.NO_APP_KEYS!= 'true' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - - build-rustdesk-lib-linux-amd64: - needs: [generate-bridge-linux, build-vcpkg-deps-linux] - name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - # use a high level qemu-user-static - job: - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "flatpak", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "appimage", - } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - steps: - - name: Maximize build space - run: | - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/dotnet - sudo apt update -y - sudo apt install qemu-user-static - - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 12 - - - name: Free Space - run: | - df - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - cache-directories: "/opt/rust-registry" - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - name: Install local registry - run: | - mkdir -p /opt/rust-registry - cargo install cargo-local-registry - - - name: Build local registry - uses: nick-fields/retry@v2 - id: build-local-registry - continue-on-error: true - with: - max_attempts: 3 - timeout_minutes: 15 - retry_on: error - command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Restore vcpkg files - uses: actions/download-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: /opt/artifacts/vcpkg/installed - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk library for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - # not ready yet - # distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - ls -l /opt/artifacts/vcpkg/installed - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/rust-registry:/opt/rust-registry" - shell: /bin/bash - install: | - apt update -y - echo -e "installing deps" - apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null - # we have libopus compiled by us. - apt remove -y libopus-dev || true - # output devs - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # rust - pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh - rm -rf rust-1.64.0-${{ matrix.job.target }} - # edit config - mkdir -p ~/.cargo/ - echo """ - [source.crates-io] - registry = 'https://github.com/rust-lang/crates.io-index' - replace-with = 'local-registry' - - [source.local-registry] - local-registry = '/opt/rust-registry/' - """ > ~/.cargo/config - cat ~/.cargo/config - # start build - pushd /workspace - # mock - case "${{ matrix.job.arch }}" in - 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 - ;; - esac - - - name: Upload Artifacts - uses: actions/upload-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: target/release/liblibrustdesk.so - - build-rustdesk-lib-linux-arm: - needs: [generate-bridge-linux, build-vcpkg-deps-linux] - name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - # use a high level qemu-user-static - job: - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-20.04, - use-cross: true, - extra-build-features: "", - } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "appimage", - } - # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { - # arch: armv7, - # target: arm-unknown-linux-gnueabihf, - # os: ubuntu-20.04, - # use-cross: true, - # extra-build-features: "", - # } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - steps: - - name: Maximize build space - run: | - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/dotnet - sudo apt update -y - sudo apt install qemu-user-static - - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 12 - - - name: Free Space - run: | - df - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - cache-directories: "/opt/rust-registry" - - - name: Install local registry - run: | - mkdir -p /opt/rust-registry - cargo install cargo-local-registry - - - name: Build local registry - uses: nick-fields/retry@v2 - id: build-local-registry - continue-on-error: true - with: - max_attempts: 3 - timeout_minutes: 15 - retry_on: error - command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Restore vcpkg files - uses: actions/download-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: /opt/artifacts/vcpkg/installed - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk library for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - ls -l /opt/artifacts/vcpkg/installed - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/rust-registry:/opt/rust-registry" - shell: /bin/bash - install: | - apt update -y - echo -e "installing deps" - apt-get -qq install -y 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 libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null - # we have libopus compiled by us. - apt remove -y libopus-dev || true - # output devs - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # rust - pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh - rm -rf rust-1.64.0-${{ matrix.job.target }} - # edit config - mkdir -p ~/.cargo/ - echo """ - [source.crates-io] - registry = 'https://github.com/rust-lang/crates.io-index' - replace-with = 'local-registry' - - [source.local-registry] - local-registry = '/opt/rust-registry/' - """ > ~/.cargo/config - cat ~/.cargo/config - # start build - pushd /workspace - # mock - case "${{ matrix.job.arch }}" in - aarch64) - cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ - cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ - ls -l /opt/artifacts/vcpkg/installed/lib/ - mkdir -p /vcpkg/installed/arm64-linux - ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib - 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 - ;; - armv7) - cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ - cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ - mkdir -p /vcpkg/installed/arm-linux - ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib - 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 - ;; - esac - - - name: Upload Artifacts - uses: actions/upload-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: target/release/liblibrustdesk.so - - build-rustdesk-linux-arm: - needs: [build-rustdesk-lib-linux-arm] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ubuntu-20.04 # 20.04 has more performance on arm build - strategy: - fail-fast: false - matrix: - job: - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "", - } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "appimage", - } - # - { - # arch: aarch64, - # target: aarch64-unknown-linux-gnu, - # os: ubuntu-18.04, # just for naming package, not running host - # use-cross: true, - # extra-build-features: "flatpak", - # } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Prepare env - run: | - sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools - mkdir -p ./target/release/ - - - name: Restore the rustdesk lib file - uses: actions/download-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/ - - - name: Download Flutter - shell: bash - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /opt - # clone repo and reset to flutter 3.0.5 - git clone https://github.com/sony/flutter-elinux.git || true - pushd flutter-elinux - # reset to flutter 3.0.5 - git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 - popd - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk binary for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/flutter-elinux:/opt/flutter-elinux" - shell: /bin/bash - install: | - apt update -y - apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /workspace - # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux - export PATH=/opt/flutter-elinux/bin:$PATH - flutter-elinux doctor -v - # edit to corresponding arch - case ${{ matrix.job.arch }} in - aarch64) - sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py - sed -i "s/x64\/release/arm64\/release/g" ./build.py - ;; - armv7) - sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py - sed -i "s/x64\/release/arm\/release/g" ./build.py - ;; - esac - python3 ./build.py --flutter --hwcodec --skip-cargo - # rpm package - echo -e "start packaging" - pushd /workspace - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec - sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter.spec - ;; - aarch64) - sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" - done - - - name: Rename rustdesk - shell: bash - run: | - for name in rustdesk*??.deb; do - cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" - done - - - name: Publish debian package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Build appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - shell: bash - run: | - # set-up appimage-builder - pushd /tmp - wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage - chmod +x appimage-builder-x86_64.AppImage - sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder - popd - # run appimage-builder - pushd appimage - sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml - - - name: Publish appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - - - name: Upload Artifact - uses: actions/upload-artifact@master - if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Patch archlinux PKGBUILD - if: ${{ matrix.job.extra-build-features == '' }} - run: | - sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/linux\/x64/linux\/arm/g" ./res/PKGBUILD - ;; - aarch64) - sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD - ;; - esac - - # Temporary disable for there is no many archlinux arm hosts - # - name: Build archlinux package - # if: ${{ matrix.job.extra-build-features == '' }} - # uses: vufa/arch-makepkg-action@master - # with: - # packages: > - # llvm - # clang - # libva - # libvdpau - # rust - # gstreamer - # unzip - # git - # cmake - # gcc - # curl - # wget - # yasm - # nasm - # zip - # make - # pkg-config - # clang - # gtk3 - # xdotool - # libxcb - # libxfixes - # alsa-lib - # pipewire - # python - # ttf-arphic-uming - # libappindicator-gtk3 - # scripts: | - # cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - - # - name: Publish archlinux package - # if: ${{ matrix.job.extra-build-features == '' }} - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # res/rustdesk*.zst - - - name: Publish fedora28/centos8 package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - /opt/artifacts/rpm/*.rpm - - build-rustdesk-linux-amd64: - needs: [build-rustdesk-lib-linux-amd64] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - job: - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "flatpak", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "appimage", - } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Prepare env - run: | - sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools - mkdir -p ./target/release/ - - - name: Restore the rustdesk lib file - uses: actions/download-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/ - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk binary for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - shell: /bin/bash - install: | - apt update -y - apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # Setup Flutter - pushd /opt - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz - tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz - ls -l . - export PATH=/opt/flutter/bin:$PATH - flutter doctor -v - pushd /workspace - python3 ./build.py --flutter --hwcodec --skip-cargo - # rpm package - pushd /workspace - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" - done - - - name: Rename rustdesk - shell: bash - run: | - for name in rustdesk*??.deb; do - # use cp to duplicate deb files to fit other packages. - cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" - done - - - name: Publish debian package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Upload Artifact - uses: actions/upload-artifact@master - if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Patch archlinux PKGBUILD - if: ${{ matrix.job.extra-build-features == '' }} - run: | - sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD - - - name: Build archlinux package - if: ${{ matrix.job.extra-build-features == '' }} - uses: vufa/arch-makepkg-action@master - with: - packages: > - llvm - clang - libva - libvdpau - rust - gstreamer - unzip - git - cmake - gcc - curl - wget - yasm - nasm - zip - make - pkg-config - clang - gtk3 - xdotool - libxcb - libxfixes - alsa-lib - pipewire - python - ttf-arphic-uming - libappindicator-gtk3 - scripts: | - cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - - - name: Publish archlinux package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - res/rustdesk*.zst - - - name: Build appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - shell: bash - run: | - # set-up appimage-builder - pushd /tmp - wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage - chmod +x appimage-builder-x86_64.AppImage - sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder - popd - # run appimage-builder - pushd appimage - sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml - - - name: Publish appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - - - name: Publish fedora28/centos8 package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - /opt/artifacts/rpm/*.rpm - - # Temporary disable flatpak arm build - # - # build-flatpak-arm: - # name: Build Flatpak - # needs: [build-rustdesk-linux-arm] - # runs-on: ${{ matrix.job.os }} - # strategy: - # fail-fast: false - # matrix: - # job: - # # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, arch: arm64 } - # - { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, arch: arm64 } - # steps: - # - name: Checkout source code - # uses: actions/checkout@v3 - - # - name: Download Binary - # uses: actions/download-artifact@master - # with: - # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - # path: . - - # - name: Rename Binary - # run: | - # mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - - # - uses: Kingtous/run-on-arch-action@amd64-support - # name: Build rustdesk flatpak package for ${{ matrix.job.arch }} - # id: rpm - # with: - # arch: ${{ matrix.job.arch }} - # distro: ubuntu18.04 - # githubToken: ${{ github.token }} - # setup: | - # ls -l "${PWD}" - # dockerRunArgs: | - # --volume "${PWD}:/workspace" - # shell: /bin/bash - # install: | - # apt update -y - # apt install -y rpm - # run: | - # pushd /workspace - # # install - # apt update -y - # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git - # # flatpak deps - # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 - # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 - # # package - # pushd flatpak - # git clone https://github.com/flathub/shared-modules.git --depth=1 - # flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json - # flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk - - # - name: Publish flatpak package - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak - - build-flatpak-amd64: - name: Build Flatpak - needs: [build-rustdesk-linux-amd64] - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - job: - - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Download Binary - uses: actions/download-artifact@master - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: . - - - name: Rename Binary - run: | - mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk flatpak package for ${{ matrix.job.arch }} - id: rpm - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - shell: /bin/bash - install: | - apt update -y - apt install -y rpm git wget curl - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /workspace - # install - apt update -y - apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git - # flatpak deps - flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 - flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 - # package - pushd flatpak - git clone https://github.com/flathub/shared-modules.git --depth=1 - flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json - flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk - - - name: Publish flatpak package - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak From 7374a924737ec1e86c8249706634b85b00c51479 Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:49:35 +0200 Subject: [PATCH 130/734] Update gr.rs --- src/lang/gr.rs | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 81a50bcd8..a907de84b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -403,33 +403,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), - ("Skipped", ""), + ("Skipped", "Παράλειψη"), ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), ("Group", "Ομάδα"), ("Search", "Αναζήτηση"), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("Closed manually by the web console", "Κλειστό χειροκίνητα από την κονσόλα web"), + ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), + ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), + ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), + ("Always use software rendering", "Να χρησιμοποιείται πάντα επιτάχυνση λογισμικού"), + ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), + ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), + ("Wait", "Περιμένετε"), + ("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"), + ("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", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο OK στο παράθυρο UAC όπου εκτελείται το RustDesk."), + ("Request Elevation", "Αίτημα ανύψωσης δικαιωμάτων χρήστη"), + ("wait_accept_uac_tip", "Περιμένετε να αποδεχτεί ο απομακρυσμένος χρήστης το παράθυρο διαλόγου UAC."), + ("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"), + ("uppercase", "κεφαλαία γράμματα"), + ("lowercase", "πεζά γράμματα"), + ("digit", "ψηφίο"), + ("special character", "ειδικός χαρακτήρας"), + ("length>=8", "μήκος>=8"), + ("Weak", "Αδύναμο"), + ("Medium", "Μέτριο"), + ("Strong", "Δυνατό"), ].iter().cloned().collect(); } From 8d1f4a5f78d28085c150f06445f10e6bace9aee2 Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:29:22 +0200 Subject: [PATCH 131/734] Update gr.rs --- src/lang/gr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index a907de84b..91607877c 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -410,8 +410,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Κλειστό χειροκίνητα από την κονσόλα web"), ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), - ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), - ("Always use software rendering", "Να χρησιμοποιείται πάντα επιτάχυνση λογισμικού"), + ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), + ("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"), ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), ("Wait", "Περιμένετε"), From 8e6863ff611be2767352650b1b4d90c90569bc1d Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:38:27 +0200 Subject: [PATCH 132/734] Update gr.rs --- src/lang/gr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 91607877c..4b54ba8ad 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -425,7 +425,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"), ("uppercase", "κεφαλαία γράμματα"), ("lowercase", "πεζά γράμματα"), - ("digit", "ψηφίο"), + ("digit", "Αριθμός"), ("special character", "ειδικός χαρακτήρας"), ("length>=8", "μήκος>=8"), ("Weak", "Αδύναμο"), From 67db6bfeb77dd8e7507dc04876242a95cbee582c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 05:56:07 -0700 Subject: [PATCH 133/734] check for env variable and option for message --- src/ui_interface.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index f45216d0c..5d9dfe083 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,10 +243,11 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { - return true + let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); + if key_check != "None" and crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { + return False; } else { - return false + return True; } } From 12af9e636963d1adfcc33f23f42cc9d560c43f9a Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 06:56:09 -0700 Subject: [PATCH 134/734] update RENDEZVOUS_SERVER env check --- libs/hbb_common/src/config.rs | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index e53bda578..213da08f1 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -77,21 +77,13 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -//check for env variable RENDEZVOUS_SERVER1-3 if not use the default -pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ - match option_env!("RENDEZVOUS_SERVER1") { - Some(key) => key, - None => "rs-ny.rustdesk.com", - }, - match option_env!("RENDEZVOUS_SERVER2") { - Some(key) => key, - None => "rs-sg.rustdesk.com", - }, - match option_env!("RENDEZVOUS_SERVER3") { - Some(key) => key, - None => "rs-cn.rustdesk.com", - }, -]; +//check for env variable RENDEZVOUS_SERVER if not use the default +pub const RENDEZVOUS_SERVERS: [&'static str;3] = + match option_env!("RENDEZVOUS_SERVER") { + Some(key) => [key,key,key], + None => ["rs-ny.rustdesk.com","rs-sg.rustdesk.com","rs-cn.rustdesk.com"], + }; + //check for env variable RS_PUB_KEY if not use default pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { From f6de021d37aaeba522abceba827aa7d4559e57cc Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:04:02 -0700 Subject: [PATCH 135/734] replace and with && --- src/ui_interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 5d9dfe083..8744780bc 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -244,7 +244,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); - if key_check != "None" and crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { + if key_check != "None" && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { return False; } else { return True; From ada2d2b539a687c8846babc1e1b9e696b4fd7c9d Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:21:01 -0700 Subject: [PATCH 136/734] Update ui_interface.rs --- src/ui_interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 8744780bc..7ea97ced1 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -244,7 +244,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); - if key_check != "None" && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { + if key_check != None && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { return False; } else { return True; From f87dff262e48254008f270e5770f20cf5fdcc85c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:39:16 -0700 Subject: [PATCH 137/734] Update ui_interface.rs --- 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 7ea97ced1..ff66ccb27 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -245,9 +245,9 @@ pub fn set_peer_option(id: String, name: String, value: String) { pub fn using_public_server() -> bool { let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); if key_check != None && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { - return False; + return false; } else { - return True; + return true; } } From d601a82b5a46b961b6e6cfa08d8ac8091fc92fcf Mon Sep 17 00:00:00 2001 From: qiushiyang Date: Tue, 17 Jan 2023 22:46:11 +0800 Subject: [PATCH 138/734] Allow direct connect to {hostname}:{port} --- libs/hbb_common/src/lib.rs | 22 ++++++++++++++++++++++ src/client.rs | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 49be934fb..1069cb8cf 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -321,6 +321,13 @@ pub fn is_ip_str(id: &str) -> bool { is_ipv4_str(id) || is_ipv6_str(id) } +#[inline] +pub fn is_hostname_port_str(id: &str) -> bool { + regex::Regex::new(r"^[\-.0-9a-zA-Z]+:\d{1,5}$") + .unwrap() + .is_match(id) +} + #[cfg(test)] mod test_lib { use super::*; @@ -340,4 +347,19 @@ mod test_lib { assert_eq!(is_ipv6_str("[1:2::0]:"), false); assert_eq!(is_ipv6_str("1:2::0]:1"), false); } + #[test] + fn test_hostname_port() { + assert_eq!(is_ipv6_str("a:12"), true); + assert_eq!(is_ipv6_str("a.b.c:12"), true); + assert_eq!(is_ipv6_str("test.com:12"), true); + assert_eq!(is_ipv6_str("1.2.3:12"), true); + assert_eq!(is_ipv6_str("a.b.c:123456"), false); + // todo: should we also check for these edge case? + // out-of-range port + assert_eq!(is_ipv6_str("test.com:0"), true); + assert_eq!(is_ipv6_str("test.com:98989"), true); + // invalid hostname + assert_eq!(is_ipv6_str("---:12"), true); + assert_eq!(is_ipv6_str(".:12"), true); + } } diff --git a/src/client.rs b/src/client.rs index 8b2edbcda..f3cca9b36 100644 --- a/src/client.rs +++ b/src/client.rs @@ -181,6 +181,17 @@ impl Client { true, )); } + // Allow connect to {hostname}:{port} + if hbb_common.is_hostname_port_str(peer) { + return Ok(( + socket_client::connect_tcp( + peer, + RENDEZVOUS_TIMEOUT, + ) + .await?, + true, + )); + } let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; let mut socket = socket_client::connect_tcp(&*rendezvous_server, RENDEZVOUS_TIMEOUT).await; debug_assert!(!servers.contains(&rendezvous_server)); From 9980246b90cafc745765590a9834ee40a4832ecf Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:54:11 -0700 Subject: [PATCH 139/734] add RS_DEF_PUB_KEY --- libs/hbb_common/src/config.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 213da08f1..0d5b81b26 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -80,15 +80,17 @@ const CHARS: &'static [char] = &[ //check for env variable RENDEZVOUS_SERVER if not use the default pub const RENDEZVOUS_SERVERS: [&'static str;3] = match option_env!("RENDEZVOUS_SERVER") { - Some(key) => [key,key,key], + Some(key) => [key], None => ["rs-ny.rustdesk.com","rs-sg.rustdesk.com","rs-cn.rustdesk.com"], }; + +pub const RS_DEF_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; //check for env variable RS_PUB_KEY if not use default pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { Some(key) => key, - None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", + None => RS_DEF_PUB_KEY, }; pub const RENDEZVOUS_PORT: i32 = 21116; From 8aea21e9f55e3ae66a9edae5c1c168b92700f645 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:54:28 -0700 Subject: [PATCH 140/734] check for server with RS_DEF_PUB_KEY --- src/ui_interface.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ff66ccb27..ef26534cf 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,11 +243,10 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); - if key_check != None && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { - return false; + if hbb_common::config::RS_PUB_KEY == hbb_common::config::RS_DEF_PUB_KEY { + return true } else { - return true; + return false } } From b59ae9bd42b515f9288fa1d4896cf648c7bb66f9 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 08:03:24 -0700 Subject: [PATCH 141/734] requires 3 elements in array --- libs/hbb_common/src/config.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 0d5b81b26..d819c816d 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -80,12 +80,10 @@ const CHARS: &'static [char] = &[ //check for env variable RENDEZVOUS_SERVER if not use the default pub const RENDEZVOUS_SERVERS: [&'static str;3] = match option_env!("RENDEZVOUS_SERVER") { - Some(key) => [key], + Some(key) => [key, key, key], None => ["rs-ny.rustdesk.com","rs-sg.rustdesk.com","rs-cn.rustdesk.com"], }; - - pub const RS_DEF_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; //check for env variable RS_PUB_KEY if not use default pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { From f10f969c2c2626251011ab99df1d09c4fa972228 Mon Sep 17 00:00:00 2001 From: qiushiyang Date: Wed, 18 Jan 2023 02:08:44 +0000 Subject: [PATCH 142/734] fix syntax error --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index f3cca9b36..304314326 100644 --- a/src/client.rs +++ b/src/client.rs @@ -182,7 +182,7 @@ impl Client { )); } // Allow connect to {hostname}:{port} - if hbb_common.is_hostname_port_str(peer) { + if hbb_common::is_hostname_port_str(peer) { return Ok(( socket_client::connect_tcp( peer, From 12d446b217ed5987c43979609440c12aee24c4ce Mon Sep 17 00:00:00 2001 From: qiushiyang Date: Wed, 18 Jan 2023 03:35:13 +0000 Subject: [PATCH 143/734] fix test for is_hostname_port_str --- libs/hbb_common/src/lib.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 1069cb8cf..29b066c66 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -347,19 +347,20 @@ mod test_lib { assert_eq!(is_ipv6_str("[1:2::0]:"), false); assert_eq!(is_ipv6_str("1:2::0]:1"), false); } + #[test] fn test_hostname_port() { - assert_eq!(is_ipv6_str("a:12"), true); - assert_eq!(is_ipv6_str("a.b.c:12"), true); - assert_eq!(is_ipv6_str("test.com:12"), true); - assert_eq!(is_ipv6_str("1.2.3:12"), true); - assert_eq!(is_ipv6_str("a.b.c:123456"), false); + assert_eq!(is_hostname_port_str("a:12"), true); + assert_eq!(is_hostname_port_str("a.b.c:12"), true); + assert_eq!(is_hostname_port_str("test.com:12"), true); + assert_eq!(is_hostname_port_str("1.2.3:12"), true); + assert_eq!(is_hostname_port_str("a.b.c:123456"), false); // todo: should we also check for these edge case? // out-of-range port - assert_eq!(is_ipv6_str("test.com:0"), true); - assert_eq!(is_ipv6_str("test.com:98989"), true); + assert_eq!(is_hostname_port_str("test.com:0"), true); + assert_eq!(is_hostname_port_str("test.com:98989"), true); // invalid hostname - assert_eq!(is_ipv6_str("---:12"), true); - assert_eq!(is_ipv6_str(".:12"), true); + assert_eq!(is_hostname_port_str("---:12"), true); + assert_eq!(is_hostname_port_str(".:12"), true); } } From 7aced73393258fe29563de8785e27f83dc38bd8a Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 18 Jan 2023 11:48:10 +0800 Subject: [PATCH 144/734] Revert "Allow setting custom server and key with env variables" --- .github/workflows/flutter-nightly.yml | 24 +----------------------- libs/hbb_common/src/config.rs | 20 ++++++-------------- src/ui_interface.rs | 6 +----- 3 files changed, 8 insertions(+), 42 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 112cb53ad..845ba339b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,11 +15,6 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - # To make a custom build with your own servers set the below secret values - RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' - RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' - RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' - RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' jobs: build-for-windows: @@ -155,7 +150,6 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -163,13 +157,11 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -178,7 +170,6 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -249,7 +240,6 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -562,7 +552,6 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -575,14 +564,12 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish signed apk package - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + - name: Publish apk package uses: softprops/action-gh-release@v1 with: prerelease: true @@ -590,15 +577,6 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish unsigned apk package - if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index d819c816d..1d427a2e9 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -77,20 +77,12 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -//check for env variable RENDEZVOUS_SERVER if not use the default -pub const RENDEZVOUS_SERVERS: [&'static str;3] = - match option_env!("RENDEZVOUS_SERVER") { - Some(key) => [key, key, key], - None => ["rs-ny.rustdesk.com","rs-sg.rustdesk.com","rs-cn.rustdesk.com"], - }; - -pub const RS_DEF_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; -//check for env variable RS_PUB_KEY if not use default -pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { - Some(key) => key, - None => RS_DEF_PUB_KEY, -}; - +pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ + "rs-ny.rustdesk.com", + "rs-sg.rustdesk.com", + "rs-cn.rustdesk.com", +]; +pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ef26534cf..9984198b8 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,11 +243,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - if hbb_common::config::RS_PUB_KEY == hbb_common::config::RS_DEF_PUB_KEY { - return true - } else { - return false - } + crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } #[inline] From aa2cd37fb35c791bb9906d80b69206d311d78a11 Mon Sep 17 00:00:00 2001 From: qiushiyang Date: Wed, 18 Jan 2023 06:08:46 +0000 Subject: [PATCH 145/734] use more accurate regex for {domain}:{port} --- libs/hbb_common/src/lib.rs | 40 ++++++++++++++++++++++++-------------- src/client.rs | 12 ++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 29b066c66..e57994f34 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -322,10 +322,15 @@ pub fn is_ip_str(id: &str) -> bool { } #[inline] -pub fn is_hostname_port_str(id: &str) -> bool { - regex::Regex::new(r"^[\-.0-9a-zA-Z]+:\d{1,5}$") - .unwrap() - .is_match(id) +pub fn is_domain_port_str(id: &str) -> bool { + // modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname. + // according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700, + // there is no digits in TLD, and length is 2~63. + regex::Regex::new( + r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$", + ) + .unwrap() + .is_match(id) } #[cfg(test)] @@ -350,17 +355,22 @@ mod test_lib { #[test] fn test_hostname_port() { - assert_eq!(is_hostname_port_str("a:12"), true); - assert_eq!(is_hostname_port_str("a.b.c:12"), true); - assert_eq!(is_hostname_port_str("test.com:12"), true); - assert_eq!(is_hostname_port_str("1.2.3:12"), true); - assert_eq!(is_hostname_port_str("a.b.c:123456"), false); - // todo: should we also check for these edge case? + assert_eq!(is_domain_port_str("a:12"), false); + assert_eq!(is_domain_port_str("a.b.c:12"), false); + assert_eq!(is_domain_port_str("test.com:12"), true); + assert_eq!(is_domain_port_str("test-UPPER.com:12"), true); + assert_eq!(is_domain_port_str("some-other.domain.com:12"), true); + assert_eq!(is_domain_port_str("under_score:12"), false); + assert_eq!(is_domain_port_str("a@bc:12"), false); + assert_eq!(is_domain_port_str("1.1.1.1:12"), false); + assert_eq!(is_domain_port_str("1.2.3:12"), false); + assert_eq!(is_domain_port_str("1.2.3.45:12"), false); + assert_eq!(is_domain_port_str("a.b.c:123456"), false); + assert_eq!(is_domain_port_str("---:12"), false); + assert_eq!(is_domain_port_str(".:12"), false); + // todo: should we also check for these edge cases? // out-of-range port - assert_eq!(is_hostname_port_str("test.com:0"), true); - assert_eq!(is_hostname_port_str("test.com:98989"), true); - // invalid hostname - assert_eq!(is_hostname_port_str("---:12"), true); - assert_eq!(is_hostname_port_str(".:12"), true); + assert_eq!(is_domain_port_str("test.com:0"), true); + assert_eq!(is_domain_port_str("test.com:98989"), true); } } diff --git a/src/client.rs b/src/client.rs index 304314326..493448c3b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,10 +7,10 @@ use cpal::{ use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use std::{ - str::FromStr, collections::HashMap, net::SocketAddr, ops::{Deref, Not}, + str::FromStr, sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; use uuid::Uuid; @@ -181,14 +181,10 @@ impl Client { true, )); } - // Allow connect to {hostname}:{port} - if hbb_common::is_hostname_port_str(peer) { + // Allow connect to {domain}:{port} + if hbb_common::is_domain_port_str(peer) { return Ok(( - socket_client::connect_tcp( - peer, - RENDEZVOUS_TIMEOUT, - ) - .await?, + socket_client::connect_tcp(peer, RENDEZVOUS_TIMEOUT).await?, true, )); } From 8fb3c452bea8dfb9c1de14db8b06f158d31147dd Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 18 Jan 2023 14:22:41 +0800 Subject: [PATCH 146/734] Allow setting custom server and key with env variables #2810 --- .github/workflows/flutter-nightly.yml | 2 ++ libs/hbb_common/src/config.rs | 14 +++++++++++--- src/platform/windows.rs | 16 ---------------- src/ui.rs | 5 ----- src/ui/index.tis | 1 - src/ui_interface.rs | 10 ++-------- 6 files changed, 15 insertions(+), 33 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 845ba339b..d5782eabf 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,6 +15,8 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' jobs: build-for-windows: diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1d427a2e9..abec5b231 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -49,7 +49,10 @@ lazy_static::lazy_static! { static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::load())); pub static ref ONLINE: Arc>> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); + pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Arc::new(RwLock::new(match option_env!("RENDEZVOUS_SERVER") { + Some(key) => key, + _ => "", + }.to_owned())); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); @@ -77,12 +80,17 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ +const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-ny.rustdesk.com", "rs-sg.rustdesk.com", "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; + +pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { + Some(key) => key, + None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", +}; + pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 89861a418..190834eb8 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1368,22 +1368,6 @@ pub fn get_license() -> Option { pub fn bootstrap() { if let Some(lic) = get_license() { *config::PROD_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); - #[cfg(feature = "hbbs")] - { - if !is_win_server() { - return; - } - crate::hbbs::bootstrap(&lic.key, &lic.host); - std::thread::spawn(move || loop { - let tmp = Config::get_option("stop-rendezvous-service"); - if tmp.is_empty() { - crate::hbbs::start(); - } else { - crate::hbbs::stop(); - } - std::thread::sleep(std::time::Duration::from_millis(100)); - }); - } } } diff --git a/src/ui.rs b/src/ui.rs index d45a64298..4cd9ce3f7 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -208,10 +208,6 @@ impl UI { show_run_without_install() } - fn has_rendezvous_service(&self) -> bool { - has_rendezvous_service() - } - fn get_license(&self) -> String { get_license() } @@ -599,7 +595,6 @@ impl sciter::EventHandler for UI { fn peer_has_password(String); fn forget_password(String); fn set_peer_option(String, String, String); - fn has_rendezvous_service(); fn get_license(); fn test_if_valid_server(String); fn get_sound_inputs(); diff --git a/src/ui/index.tis b/src/ui/index.tis index c141d0efe..774b6184a 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -310,7 +310,6 @@ class MyIdMenu: Reactor.Component { {handler.is_rdp_service_open() ? : ""} {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connected via relay')}
  • } - {handler.has_rendezvous_service() ?
  • {translate(rendezvous_service_stopped ? "Start ID/relay service" : "Stop ID/relay service")}
  • : ""} {handler.is_ok_change_id() ?
    : ""} {username ?
  • {translate('Logout')} ({username})
  • : diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9984198b8..2e6ef561c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -134,13 +134,6 @@ pub fn show_run_without_install() -> bool { false } -#[inline] -pub fn has_rendezvous_service() -> bool { - #[cfg(all(windows, feature = "hbbs"))] - return crate::platform::is_win_server() && crate::platform::windows::get_license().is_some(); - return false; -} - #[inline] pub fn get_license() -> String { #[cfg(windows)] @@ -243,7 +236,8 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() + option_env!("RENDEZVOUS_SERVER").is_none() + && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } #[inline] From b4a88c7780acd1942457ef662dd3b68efe4e3022 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 18 Jan 2023 14:42:08 +0800 Subject: [PATCH 147/734] fix CI --- src/flutter_ffi.rs | 4 ---- src/ui/index.tis | 8 -------- 2 files changed, 12 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a4b6d1395..ca6823aa5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -619,10 +619,6 @@ pub fn main_discover() { discover(); } -pub fn main_has_rendezvous_service() -> bool { - has_rendezvous_service() -} - pub fn main_get_api_server() -> String { get_api_server() } diff --git a/src/ui/index.tis b/src/ui/index.tis index 774b6184a..2d77b1eec 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -9,7 +9,6 @@ var app; var tmp = handler.get_connect_status(); var connect_status = tmp[0]; var service_stopped = handler.get_option("stop-service") == "Y"; -var rendezvous_service_stopped = false; var using_public_server = handler.using_public_server(); var software_update_url = ""; var key_confirmed = tmp[1]; @@ -467,8 +466,6 @@ class MyIdMenu: Reactor.Component { }, 240); } else if (me.id == "stop-service") { handler.set_option("stop-service", service_stopped ? "" : "Y"); - } else if (me.id == "stop-rendezvous-service") { - handler.set_option("stop-rendezvous-service", rendezvous_service_stopped ? "" : "Y"); } else if (me.id == "change-id") { msgbox("custom-id", translate("Change ID"), "
    \
    " + translate('id_change_tip') + "
    \ @@ -1120,11 +1117,6 @@ function checkConnectStatus() { service_stopped = tmp; app.update(); } - tmp = !!handler.get_option("stop-rendezvous-service"); - if (tmp != rendezvous_service_stopped) { - rendezvous_service_stopped = tmp; - myIdMenu.update(); - } tmp = handler.using_public_server(); if (tmp != using_public_server) { using_public_server = tmp; From 134be63d116804c9bac0969cb1b3b2ab09f32fbe Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 18 Jan 2023 10:12:32 +0300 Subject: [PATCH 148/734] update ru.rs --- src/lang/ru.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b22d49cc2..abe642d9c 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -51,7 +51,7 @@ 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"), + ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Invalid format", "Неправильный формат"), ("server_not_support", "Пока не поддерживается сервером"), ("Not available", "Недоступно"), @@ -413,23 +413,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), + ("Wait", "Ждите"), + ("Elevation Error", "Ошибка повышения прав"), + ("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", "По-прежнему требуется, чтобы удалённый пользователь нажал \"OK\" в окне UAC при запуске RustDesk."), + ("Request Elevation", "Запросить повышение"), + ("wait_accept_uac_tip", "Подождите, пока удалённый пользователь подтвердит запрос UAC."), + ("Elevate successfully", "Права повышены"), + ("uppercase", "заглавные"), + ("lowercase", "строчные"), + ("digit", "цифры"), + ("special character", "спецсимволы"), + ("length>=8", "8+ символов"), + ("Weak", "Слабый"), + ("Medium", "Средний"), + ("Strong", "Стойкий"), ].iter().cloned().collect(); } From 9fa76d23490ac3a6dd955fdcdab950a5d05e115a Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 09:38:02 +0100 Subject: [PATCH 149/734] Update it.rs --- src/lang/it.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index f94669d34..cdc44d640 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -423,13 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Richiedi elevazione dei diritti"), ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("uppercase", "maiuscolo"), + ("lowercase", "minuscolo"), + ("digit", "numero"), + ("special character", "carattere speciale"), + ("length>=8", "lunghezza >= 8"), + ("Weak", "Debole"), + ("Medium", "Media"), + ("Strong", "Forte"), ].iter().cloned().collect(); } From 6aafe386b2953c09e4decb3b195cc92babd7d6c6 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 09:45:53 +0100 Subject: [PATCH 150/734] 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 cdc44d640..e37985d8b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -39,7 +39,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambia ID"), ("Website", "Sito web"), ("About", "Informazioni"), - ("Slogan_tip", ""), + ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), ("Privacy Statement", ""), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), From 845b2a943ed23621957232fb1ccac0152252b34e Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:01:19 +0100 Subject: [PATCH 151/734] Update it.rs --- src/lang/it.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index e37985d8b..460b26aa0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -40,7 +40,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Website", "Sito web"), ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), - ("Privacy Statement", ""), + ("Privacy Statement", "Informativa sulla privacy"), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), @@ -185,7 +185,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter your password", "Inserisci la tua password"), ("Logging in...", "Autenticazione..."), ("Enable RDP session sharing", "Abilita la condivisione della sessione RDP"), - ("Auto Login", "Login automatico"), + ("Auto Login", "Accesso automatico"), ("Enable Direct IP Access", "Abilita l'accesso diretto tramite IP"), ("Rename", "Rinomina"), ("Space", "Spazio"), @@ -195,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Inserisci il nome della cartella"), ("Fix it", "Risolvi"), ("Warning", "Avviso"), - ("Login screen using Wayland is not supported", "La schermata di login non è supportata utilizzando Wayland"), + ("Login screen using Wayland is not supported", "La schermata di accesso non è supportata utilizzando Wayland"), ("Reboot required", "Riavvio necessario"), ("Unsupported display server ", "Display server non supportato"), ("x11 expected", "x11 necessario"), @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Connetti sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Verifica"), + ("Remember me", "Ricordami"), + ("Trust this device", "Registra questo dispositivo come attendibile"), + ("Verification code", "Codice di verifica"), + ("verification_tip", "È stato rilevato un nuovo dispositivo e un codice di verifica è stato inviato all'indirizzo e-mail registrato; inserire il codice di verifica per continuare l'accesso."), ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), From 8f31378155e763f4eef641364ce6ed6dad73f5c2 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:09:10 +0100 Subject: [PATCH 152/734] Update it.rs --- src/lang/it.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 460b26aa0..d2358a336 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -202,12 +202,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Port", "Porta"), ("Settings", "Impostazioni"), ("Username", " Nome utente"), - ("Invalid port", "Porta non valida"), + ("Invalid port", "Numero di porta non valido"), ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), - ("Run without install", "Avvia senza installare"), + ("Run without install", "Esegui senza installare"), ("Always connected via relay", "Connesso sempre tramite relay"), - ("Always connect via relay", "Connetti sempre tramite relay"), + ("Always connect via relay", "Collegati sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), ("Verify", "Verifica"), @@ -224,8 +224,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add Tag", "Aggiungi tag"), ("Unselect all tags", "Deseleziona tutti i tag"), ("Network error", "Errore di rete"), - ("Username missed", "Nome utente dimenticato"), - ("Password missed", "Password dimenticata"), + ("Username missed", "Nome utente mancante"), + ("Password missed", "Password mancante"), ("Wrong credentials", "Credenziali errate"), ("Edit Tag", "Modifica tag"), ("Unremember Password", "Dimentica password"), From 22412acfccaf708e7e89a29f8eac12e55b0fafab Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:18:06 +0100 Subject: [PATCH 153/734] Update it.rs --- src/lang/it.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index d2358a336..e3c1a1f0f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -332,9 +332,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Menubar", "Mostra la barra dei menu"), ("Hide Menubar", "nascondi la barra dei menu"), ("Direct Connection", "Connessione diretta"), - ("Relay Connection", "Collegamento a relè"), + ("Relay Connection", "Connessione relay"), ("Secure Connection", "Connessione sicura"), - ("Insecure Connection", "Connessione insicura"), + ("Insecure Connection", "Connessione non sicura"), ("Scale original", "Scala originale"), ("Scale adaptive", "Scala adattiva"), ("General", "Generale"), @@ -348,9 +348,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unlock Security Settings", "Sblocca impostazioni di sicurezza"), ("Enable Audio", "Abilita audio"), ("Unlock Network Settings", "Sblocca impostazioni di rete"), - ("Server", ""), + ("Server", "Server"), ("Direct IP Access", "Accesso IP diretto"), - ("Proxy", ""), + ("Proxy", "Proxy"), ("Apply", "Applica"), ("Disconnect all devices?", "Disconnettere tutti i dispositivi?"), ("Clear", "Ripulisci"), @@ -372,7 +372,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Abilita il rilevamento della LAN"), ("Deny LAN Discovery", "Nega il rilevamento della LAN"), ("Write a message", "Scrivi un messaggio"), - ("Prompt", ""), + ("Prompt", "Prompt"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), ("elevated_foreground_window_tip", "La finestra corrente del desktop remoto richiede privilegi più elevati per funzionare, quindi non è in grado di utilizzare temporaneamente il mouse e la tastiera. È possibile chiedere all'utente remoto di ridurre a icona la finestra corrente o di fare clic sul pulsante di elevazione nella finestra di gestione della connessione. Per evitare questo problema, si consiglia di installare il software sul dispositivo remoto."), ("Disconnected", "Disconnesso"), From fca833fd00574f40e7d149941564b9a1fee49f73 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 02:34:53 -0700 Subject: [PATCH 154/734] fix key check in nightly yaml --- .github/workflows/flutter-nightly.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index d5782eabf..99bb20d48 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,8 +15,8 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' - RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' + # To make a custom build with your own servers set the below secret values + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY_VAL }}' jobs: build-for-windows: @@ -152,6 +152,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -159,11 +160,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -172,6 +175,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool + if: ${{ env.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -242,6 +246,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -554,6 +559,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -566,12 +572,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish apk package + - name: Publish signed apk package + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -579,6 +587,15 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + - name: Publish unsigned apk package + if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] From ac6797e4806bff8144886ccb168879f54d90eea1 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 02:35:35 -0700 Subject: [PATCH 155/734] RS_PUB_KEY --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 99bb20d48..cb32ac9d4 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -16,7 +16,7 @@ env: VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" # To make a custom build with your own servers set the below secret values - RS_PUB_KEY: '${{ secrets.RS_PUB_KEY_VAL }}' + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' jobs: build-for-windows: From c8d1480e4e08d21e9018cc81576f1c06715fbc9f Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 02:36:05 -0700 Subject: [PATCH 156/734] add RENDEZVOUS_SERVER --- .github/workflows/flutter-nightly.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index cb32ac9d4..0d1571d9d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -17,6 +17,7 @@ env: VERSION: "1.2.0" # To make a custom build with your own servers set the below secret values RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' jobs: build-for-windows: From 7f42404385e68b15fb36936a996f314222565cf2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 18 Jan 2023 11:31:32 +0800 Subject: [PATCH 157/734] feat: add suse nightly build --- .github/workflows/flutter-nightly.yml | 33 +++++++++- res/rpm-flutter-suse.spec | 87 +++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 res/rpm-flutter-suse.spec diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index d5782eabf..f03fcce54 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1041,7 +1041,7 @@ jobs: esac python3 ./build.py --flutter --hwcodec --skip-cargo # rpm package - echo -e "start packaging" + echo -e "start packaging fedora package" pushd /workspace case ${{ matrix.job.arch }} in armv7) @@ -1058,6 +1058,24 @@ jobs: for name in rustdesk*??.rpm; do mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" done + # rpm suse package + echo -e "start packaging suse package" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter-suse.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" + done - name: Rename rustdesk shell: bash @@ -1264,6 +1282,19 @@ jobs: for name in rustdesk*??.rpm; do mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" done + # rpm suse package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" + done - name: Rename rustdesk shell: bash diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec new file mode 100644 index 000000000..6c7055b4a --- /dev/null +++ b/res/rpm-flutter-suse.spec @@ -0,0 +1,87 @@ +Name: rustdesk +Version: 1.2.0 +Release: 0 +Summary: RPM package +License: GPL-3.0 +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libappindicator-gtk3 libvdpau1 libva2 +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit) + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +# %global __python %{__python3} + +%install + +mkdir -p "%{buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/lib/rustdesk" +mkdir -p "%{buildroot}/usr/bin" +install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/rustdesk/files/rustdesk.png" + +%files +/usr/lib/rustdesk/* +/usr/share/rustdesk/files/rustdesk.service +/usr/share/rustdesk/files/rustdesk.png +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop + +%changelog +# let's skip this for now + +# https://www.cnblogs.com/xingmuxin/p/8990255.html +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + rm /usr/bin/rustdesk || true + update-desktop-database + ;; + 1) + # for upgrade + ;; +esac From 6044f884eba40f98b12c6071456fba38bb067798 Mon Sep 17 00:00:00 2001 From: skycommand <17097175+skycommand@users.noreply.github.com> Date: Wed, 18 Jan 2023 20:55:36 +0330 Subject: [PATCH 158/734] Significantly improved the translation --- docs/README-FA.md | 72 +++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/README-FA.md b/docs/README-FA.md index d86c82836..02b156dbb 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -1,60 +1,60 @@ -

    +

    RustDesk - Your remote desktop
    - اسنپ شات • - ساختار • - داکر • - ساخت • - سرور
    - [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    - ‫برای ترجمه این RustDesk UI ،README و Doc به زبان مادری شما به کمکتون نیاز داریم + تصاویر محیط نرم‌افزار • + ساختار • + داکر • + ساخت • + سرور

    +

    [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]

    +

    برای ترجمه این سند (README)، رابط کاربری RustDesk، و مستندات آن به زبان مادری شما به کمکتان نیازمندیم.

    با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -یک نرم افزار دیگر کنترل دسکتاپ از راه دور، که با Rust نوشته شده است. راه اندازی سریع وبدون نیاز به تنظیمات. شما کنترل کاملی بر داده های خود دارید، بدون هیچ گونه نگرانی امنیتی. +راست‌دسک (RustDesk) نرم‌افزاری برای گارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. + می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا [ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). -‫راست دسک (RustDesk) از مشارکت همه استقبال می کند. برای راهنمایی جهت مشارکت به [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. +ما از مشارکت همه استقبال می کنیم. برای راهنمایی جهت مشارکت به[`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. -[راست دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[راست‌دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) -[دانلود باینری](https://github.com/rustdesk/rustdesk/releases) +[دریافت نرم‌افزار](https://github.com/rustdesk/rustdesk/releases) ## سرورهای عمومی رایگان -سرورهایی زیر را به صورت رایگان میتوانید استفاده می کنید. این لیست ممکن است در طول زمان تغییر کند. اگر به این سرورها نزدیک نیستید، ممکن است سرویس شما کند شود. +شما مي‌توانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر می‌کند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد. | موقعیت | سرویس دهنده | مشخصات | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | -| Germany | Hetzner | 2 vCPU / 4GB RAM | -| Germany | Codext | 4 vCPU / 8GB RAM | -| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | -| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| کره‌ی جنوبی، سئول | AWS lightsail | 1 vCPU / 0.5GB RAM | +| آلمان | Hetzner | 2 vCPU / 4GB RAM | +| آلمان | Codext | 4 vCPU / 8GB RAM | +| فنلاند، هلسینکی | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| ایالات متحده، اَشبرن | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## وابستگی ها -نسخه‌های دسکتاپ از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند، لطفا کتابخانه پویا sciter را خودتان دانلود کنید. +نسخه‌های رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند. خواهشمندیم کتابخانه‌ی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید. -[ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | -[لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | -[مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) +- [ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) +- [لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) +- [مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -نسخه های موبایل از Flutter استفاده می کنند. بعداً نسخه دسکتاپ را از Sciter به Flutter منتقل خواهیم کرد. +نسخه های همراه از Flutter استفاده می کنند. نسخه‌ی رومیزی را هم از Sciter به Flutter منتقل خواهیم کرد. -## مراحل بنیادین برای ساخت +## نیازمندی‌های ساخت -‫- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید +- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید -‫- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید: - - - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` - - Linux/MacOS: `vcpkg install libvpx libyuv opus` - -- run `cargo run` +- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید. +- بسته‌های vcpkg مورد نیاز را نصب کنید: + - ویندوز: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` + - مک و لینوکس: `vcpkg install libvpx libyuv opus` +- این دستور را اجرا کنید: `cargo run` ## [ساخت](https://rustdesk.com/docs/en/dev/build/) @@ -118,11 +118,11 @@ VCPKG_ROOT=$HOME/vcpkg cargo run ### تغییر Wayland به (X11 (Xorg -راست دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید. +راست‌دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید. ## نحوه ساخت با داکر -این مخزن گیت را کلون کنید و کانتینر را به روش زیر بسازید +این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید ```sh git clone https://github.com/rustdesk/rustdesk @@ -130,13 +130,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -سپس، هر بار که نیاز به ساخت اپلیکیشن داشتید، دستور زیر را اجرا کنید: +سپس، هر بار که نیاز به ساخت ترم‌افزار داشتید، دستور زیر را اجرا کنید: ```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 ``` -توجه داشته باشید که ساخت اول ممکن است قبل از کش شدن وابستگی ها بیشتر طول بکشد، دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور: +توجه داشته باشید که نخستین ساخت ممکن است به دلیل محلی نبودن وابستگی‌ها بیشتر طول بکشد. اما دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور: ```sh target/debug/rustdesk @@ -163,7 +163,7 @@ target/release/rustdesk - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client -## اسکرین شات ها +## تصاویر محیط نرم‌افزار ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 92d009b93d8c94685b3822ea19f8e7b60fd3732b Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:18:02 -0700 Subject: [PATCH 159/734] replace env with secrets for consistency. --- .github/workflows/flutter-nightly.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 0d1571d9d..2b0e492c3 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -153,7 +153,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -161,13 +161,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -176,7 +176,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -247,7 +247,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -560,7 +560,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -573,14 +573,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -589,7 +589,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} + if: ${{ secrets.ANDROID_SIGNING_KEY!= 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 4f5b359cfce149b84b3b09c1ecc8708177443e00 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:25:43 -0700 Subject: [PATCH 160/734] env not secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You can only use the env context in the value of the with and name keys, or in a step’s if conditional, the secret value is not defined yet as its before the with. --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 2b0e492c3..65bb6f6c8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -153,7 +153,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} From fd346edebd406d22e02255d08a92988780cadc27 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:28:57 -0700 Subject: [PATCH 161/734] env not secret must use env. not secret in if's --- .github/workflows/flutter-nightly.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 65bb6f6c8..0d1571d9d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -161,13 +161,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -176,7 +176,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -247,7 +247,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -560,7 +560,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -573,14 +573,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -589,7 +589,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ secrets.ANDROID_SIGNING_KEY!= 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 5a214d91852c7da9593f357d30d9db9d522a4dd1 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:38:41 -0700 Subject: [PATCH 162/734] set env values for if's --- .github/workflows/flutter-nightly.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 0d1571d9d..466ad3d59 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,6 +15,9 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + #signing keys + ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' + MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}' # To make a custom build with your own servers set the below secret values RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' From 74a7523662d58bfd5cc0b036fc17fe22d21f7df5 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:45:17 -0700 Subject: [PATCH 163/734] fix env.MACOS_P12_BASE64 --- .github/workflows/flutter-nightly.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 466ad3d59..e1bd90593 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -156,7 +156,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -164,13 +164,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -179,7 +179,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null shell: bash run: | pushd /tmp @@ -250,7 +250,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain From 9e43a071764896071661e4f3711f4f02aec6402e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:50:32 -0700 Subject: [PATCH 164/734] update ANDROID_SIGNING_KEY --- .github/workflows/flutter-nightly.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index e1bd90593..393df44f7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -563,7 +563,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: env.ANDROID_SIGNING_KEY != null id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -576,14 +576,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: env.ANDROID_SIGNING_KEY != null uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: env.ANDROID_SIGNING_KEY != null uses: softprops/action-gh-release@v1 with: prerelease: true @@ -592,7 +592,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} + if: env.ANDROID_SIGNING_KEY != null uses: softprops/action-gh-release@v1 with: prerelease: true From 5e9b9d52087e5a0adc64a3da48acae4f16bb21a4 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:50:57 -0700 Subject: [PATCH 165/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 393df44f7..a782de229 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,7 +15,7 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - #signing keys + #signing keys env variable checks ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}' # To make a custom build with your own servers set the below secret values From d58b834c4c7bcbb7e6f16604358d08a6a820e471 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:58:00 -0700 Subject: [PATCH 166/734] verify .secrets --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index a782de229..84b0c13c7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -156,7 +156,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: env.MACOS_P12_BASE64 != null + if: secrets.MACOS_P12_BASE64 != null uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} From 86885eb5b4d8ee2e6b2bcadb3a5ddeb3cb296490 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:59:07 -0700 Subject: [PATCH 167/734] .secrets doesnt work in if --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 84b0c13c7..a782de229 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -156,7 +156,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: secrets.MACOS_P12_BASE64 != null + if: env.MACOS_P12_BASE64 != null uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} From fac375c017d1b79b014572c111e4579b7859bcb5 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 20:29:51 -0700 Subject: [PATCH 168/734] fix unsigned app publish --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index a782de229..08b1af79b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -592,7 +592,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: env.ANDROID_SIGNING_KEY != null + if: env.ANDROID_SIGNING_KEY == null uses: softprops/action-gh-release@v1 with: prerelease: true From 75c43a01fab192eae8d7aa9176d73e0d2073dd3f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Jan 2023 18:23:04 +0800 Subject: [PATCH 169/734] mac try x2 png not work, revert --- src/tray.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 76dcf3c21..e4203fec6 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -214,14 +214,15 @@ pub fn make_tray() { let mode = dark_light::detect(); let icon_path = match mode { dark_light::Mode::Dark => { - if f > 1. { + // still show big overflow icon in my test, so still use x1 png. + if f > 2. { "mac-tray-light-x2.png" } else { "mac-tray-light.png" } } dark_light::Mode::Light => { - if f > 1. { + if f > 2. { "mac-tray-dark-x2.png" } else { "mac-tray-dark.png" From 34f167b5fce278485fce80ee9e3dd38157c4d2fe Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Jan 2023 18:25:13 +0800 Subject: [PATCH 170/734] let's do it with objc with svg support later. --- src/tray.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tray.rs b/src/tray.rs index e4203fec6..6e84076c4 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -215,6 +215,7 @@ pub fn make_tray() { 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. if f > 2. { "mac-tray-light-x2.png" } else { From 7c834a6001011f401595a9d49c006344c4bcff40 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Jan 2023 18:26:59 +0800 Subject: [PATCH 171/734] more comment --- src/tray.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tray.rs b/src/tray.rs index 6e84076c4..e41a616de 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -216,6 +216,7 @@ pub fn make_tray() { 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 { From 941a567d30ace75c94cb3ef6c38e94fe40f77024 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 19 Jan 2023 16:17:30 +0100 Subject: [PATCH 172/734] Update es.rs A lot of new terms and corrections. --- src/lang/es.rs | 58 +++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index bae1b5cbf..5ab59b946 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -40,7 +40,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Website", "Sitio web"), ("About", "Acerca de"), ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), - ("Privacy Statement", ""), + ("Privacy Statement", "Declaración de privacidad"), ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Verificar"), + ("Remember me", "Recordarme"), + ("Trust this device", "Confiar en este dispositivo"), + ("Verification code", "Código de verificación"), + ("verification_tip", "Se ha detectado un nuevo dispositivo y se ha enviado un código de verificación a la dirección de correo registrada. Introduzca el código de verificación para continuar con el inicio de sesión."), ("Logout", "Salir"), ("Tags", "Tags"), ("Search ID", "Buscar ID"), @@ -305,7 +305,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Idioma"), ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), - ("android_open_battery_optimizations_tip", ""), + ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), @@ -318,8 +318,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart Remote Device", "Reiniciar dispositivo"), ("Are you sure you want to restart", "Esta Seguro que desea reiniciar?"), ("Restarting Remote Device", "Reiniciando dispositivo remoto"), - ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."), - ("Copied", ""), + ("remote_restarting_tip", "El dispositivo remoto se está reiniciando. Por favor cierre este mensaje y vuelva a conectarse con la contraseña peremanente en unos momentos."), + ("Copied", "Copiado"), ("Exit Fullscreen", "Salir de pantalla completa"), ("Fullscreen", "Pantalla completa"), ("Mobile Actions", "Acciones móviles"), @@ -373,8 +373,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Denegar descubrimiento de LAN"), ("Write a message", "Escribir un mensaje"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"), + ("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."), ("Disconnected", "Desconectado"), ("Other", "Otro"), ("Confirm before closing multiple tabs", "Confirmar antes de cerrar múltiples pestañas"), @@ -413,23 +413,23 @@ 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\"."), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), + ("Wait", "Esperar"), + ("Elevation Error", "Error de elevación"), + ("Ask the remote user for authentication", "Pida autenticación al usuario remoto"), + ("Choose this if the remote account is administrator", "Elegir si la cuenta remota es de administrador"), + ("Transmit the username and password of administrator", "Transmitir usuario y contraseña del administrador"), + ("still_click_uac_tip", "Aún se necesita que el usuario remoto haga click en OK en la ventana UAC del RusDesk en ejecución."), + ("Request Elevation", "Solicitar Elevación"), + ("wait_accept_uac_tip", "Por favor espere a que el usuario remoto acepte el diálogo UAC."), + ("Elevate successfully", "Elevar con éxito"), + ("uppercase", "mayúsculas"), + ("lowercase", "minúsculas"), + ("digit", "dígito"), + ("special character", "carácter especial"), + ("length>=8", "longitud>=8"), + ("Weak", "Débil"), + ("Medium", "Media"), + ("Strong", "Fuerte"), ].iter().cloned().collect(); } From 14d7621425819bbebad1ebbd59cd2f14362c802c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 20 Jan 2023 00:09:58 +0800 Subject: [PATCH 173/734] change product name from rustdesk to RustDesk --- flutter/macos/Runner.xcodeproj/project.pbxproj | 6 +++--- flutter/macos/Runner/Configs/AppInfo.xcconfig | 2 +- flutter/macos/rustdesk.xcodeproj/project.pbxproj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 8f11a09ed..fbf52403c 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* rustdesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = rustdesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* RustDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RustDesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -127,7 +127,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* rustdesk.app */, + 33CC10ED2044A3C60003C045 /* RustDesk.app */, ); name = Products; sourceTree = ""; @@ -212,7 +212,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */; + productReference = 33CC10ED2044A3C60003C045 /* RustDesk.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig index 389ae0a70..66dbee50c 100644 --- a/flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = rustdesk +PRODUCT_NAME = RustDesk // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj index 664f88618..6c58fef3d 100644 --- a/flutter/macos/rustdesk.xcodeproj/project.pbxproj +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -84,7 +84,7 @@ "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; ONLY_ACTIVE_ARCH = YES; - PRODUCT_NAME = rustdesk; + PRODUCT_NAME = RustDesk; SDKROOT = macosx; SUPPORTS_MACCATALYST = YES; }; @@ -105,7 +105,7 @@ "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios; "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; - PRODUCT_NAME = rustdesk; + PRODUCT_NAME = RustDesk; SDKROOT = macosx; SUPPORTS_MACCATALYST = YES; OTHER_LDFLAGS = ( From aac12c2b21be369354a5df3e5074cfe96284863f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 20 Jan 2023 01:25:15 +0800 Subject: [PATCH 174/734] applicationShouldOpenUntitledFile --- .../xcshareddata/xcschemes/Runner.xcscheme | 8 +++---- flutter/macos/Runner/AppDelegate.swift | 5 ++++ flutter/pubspec.lock | 7 ++++++ src/flutter.rs | 9 +++++++ src/platform/macos.rs | 20 ++++++++++++++++ src/ui/macos.rs | 24 ++----------------- 6 files changed, 47 insertions(+), 26 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 898fbe4e7..9c428a004 100644 --- a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 156e0c79b..46622746d 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -7,4 +7,9 @@ class AppDelegate: FlutterAppDelegate { dummy_method_to_enforce_bundling() return true } + + override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { + handle_applicationShouldOpenUntitledFile(); + return true + } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 228817422..ef57f375c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -699,6 +699,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + password_strength: + dependency: "direct main" + description: + name: password_strength + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" path: dependency: "direct main" description: diff --git a/src/flutter.rs b/src/flutter.rs index 3036ca9b3..1369b5646 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -39,6 +39,15 @@ pub extern "C" fn rustdesk_core_main() -> bool { false } +#[cfg(target_os = "macos")] +#[no_mangle] +pub extern "C" fn handle_applicationShouldOpenUntitledFile() { + hbb_common::log::debug!("icon clicked on finder"); + if std::env::args().nth(1) == Some("--server".to_owned()) { + crate::platform::macos::check_main_window(); + } +} + #[cfg(windows)] #[no_mangle] pub extern "C" fn rustdesk_core_main_args(args_len: *mut c_int) -> *mut *mut c_char { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 165470cac..c7dbd9b73 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -556,3 +556,23 @@ pub fn hide_dock() { NSApp().setActivationPolicy_(NSApplicationActivationPolicyAccessory); } } + +pub fn check_main_window() { + use sysinfo::{ProcessExt, System, SystemExt}; + let mut sys = System::new(); + sys.refresh_processes(); + let app = format!("/Applications/{}.app", crate::get_app_name()); + let my_uid = sys + .process((std::process::id() as i32).into()) + .map(|x| x.user_id()) + .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; + } + } + std::process::Command::new("open") + .args(["-n", &app]) + .status() + .ok(); +} diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 835fd87b0..7daef8eab 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -127,7 +127,7 @@ extern "C" fn application_should_handle_open_untitled_file( } hbb_common::log::debug!("icon clicked on finder"); if std::env::args().nth(1) == Some("--server".to_owned()) { - check_main_window(); + crate::platform::macos::check_main_window(); } let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); @@ -233,24 +233,4 @@ pub fn make_tray() { set_delegate(None); } crate::tray::make_tray(); -} - -pub fn check_main_window() { - use sysinfo::{ProcessExt, System, SystemExt}; - let mut sys = System::new(); - sys.refresh_processes(); - let app = format!("/Applications/{}.app", crate::get_app_name()); - let my_uid = sys - .process((std::process::id() as i32).into()) - .map(|x| x.user_id()) - .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; - } - } - std::process::Command::new("open") - .args(["-n", &app]) - .status() - .ok(); -} +} \ No newline at end of file From 45c0e10102925c16cc6882b6c9542a396d50bf73 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 20 Jan 2023 10:26:27 +0800 Subject: [PATCH 175/734] applicationDidFinishLaunching --- flutter/macos/Runner/AppDelegate.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 46622746d..5708e35cb 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -3,13 +3,21 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { + var lauched = false; override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { dummy_method_to_enforce_bundling() return true } override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { - handle_applicationShouldOpenUntitledFile(); + if (lauched) { + handle_applicationShouldOpenUntitledFile(); + } return true } + + override func applicationDidFinishLaunching(_ aNotification: Notification) { + lauched = true; + NSApplication.shared.activate(ignoringOtherApps: true); + } } From 1da141e6a7d643d401ae45cc090a267ac8fd161d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 20 Jan 2023 12:03:03 +0800 Subject: [PATCH 176/734] opt: prevent duplicate window instance on windows --- flutter/lib/common.dart | 2 +- flutter/lib/utils/multi_window_manager.dart | 2 +- flutter/windows/runner/main.cpp | 16 +++++++++------- src/core_main.rs | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 23aa9535d..fde039017 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1310,7 +1310,7 @@ bool callUniLinksUriHandler(Uri uri) { Future.delayed(Duration.zero, () { rustDeskWinManager.newRemoteDesktop(peerId); }); - return true; + return false; } return false; } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index cf6d78cd2..13e9d36bf 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -208,7 +208,7 @@ class RustDeskMultiWindowManager { } /// Remove active window which has [`windowId`] - /// + /// /// [Availability] /// This function should only be called from main window. /// For other windows, please post a unregister(hide) event to main window handler: diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 9b75aa086..d76b8c040 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -52,18 +52,20 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, free_c_args(c_args, args_len); // uni links dispatch - // only do uni links when dispatch a rustdesk links - auto prefix = std::string(uniLinksPrefix); - if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, prefix.size(), prefix.c_str()) == 0) { - HWND hwnd = ::FindWindow(_T("FLUTTER_RUNNER_WIN32_WINDOW"), _T("RustDesk")); - if (hwnd != NULL) { + HWND hwnd = ::FindWindow(_T("FLUTTER_RUNNER_WIN32_WINDOW"), _T("RustDesk")); + if (hwnd != NULL) { + if (!command_line_arguments.empty()) { + // Dispatch command line arguments DispatchToUniLinksDesktop(hwnd); - + } else { + // Not called with arguments, or just open the app shortcut on desktop. + // So we just show the main window instead. ::ShowWindow(hwnd, SW_NORMAL); ::SetForegroundWindow(hwnd); - return EXIT_FAILURE; } + return EXIT_FAILURE; } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) diff --git a/src/core_main.rs b/src/core_main.rs index 9083efe0e..7707a41c8 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -322,7 +322,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option Date: Fri, 20 Jan 2023 06:26:18 +0100 Subject: [PATCH 177/734] Update es.rs Skip = Changed from Saltar to Omitir --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 5ab59b946..6d94f8816 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -57,7 +57,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Not available", "No disponible"), ("Too frequent", "Demasiado frecuente"), ("Cancel", "Cancelar"), - ("Skip", "Saltar"), + ("Skip", "Omitir"), ("Close", "Cerrar"), ("Retry", "Reintentar"), ("OK", ""), From ba8fb027f62ff5b5daad97007ab49969553c0c4e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 19 Jan 2023 22:44:10 -0700 Subject: [PATCH 178/734] fix apk location --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index efa085ea8..95d84ba40 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -598,7 +598,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From dbbbddee495cd07e2311ca4092834585c3f0ec11 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 19 Jan 2023 23:17:07 -0700 Subject: [PATCH 179/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 95d84ba40..f3a4bb524 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -598,7 +598,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From d4789e141b081d0ba6a040d66fa3b3a0e781afc4 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 19 Jan 2023 23:41:07 -0700 Subject: [PATCH 180/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f3a4bb524..efa085ea8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -598,7 +598,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From 333092f983c3f9c5009dd924ea9de1fecf3caa8b Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Jan 2023 13:28:33 +0800 Subject: [PATCH 181/734] switch sides Signed-off-by: 21pages --- flutter/lib/common.dart | 13 ++++ flutter/lib/desktop/pages/remote_page.dart | 7 +- .../lib/desktop/pages/remote_tab_page.dart | 3 + flutter/lib/desktop/pages/server_page.dart | 13 ++++ .../lib/desktop/widgets/remote_menubar.dart | 55 +++++++++++---- flutter/lib/models/model.dart | 20 +++++- flutter/lib/models/server_model.dart | 3 + flutter/lib/utils/multi_window_manager.dart | 12 ++-- libs/hbb_common/protos/message.proto | 14 ++++ src/client.rs | 33 ++++++++- src/client/io_loop.rs | 4 ++ src/flutter.rs | 66 +++++++++++++---- src/flutter_ffi.rs | 17 +++-- src/ipc.rs | 2 + 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/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.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 + src/server/connection.rs | 70 ++++++++++++++----- src/ui/remote.rs | 4 +- src/ui_cm_interface.rs | 17 ++++- src/ui_session_interface.rs | 19 +++++ 49 files changed, 373 insertions(+), 61 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 23aa9535d..accbdf8df 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1606,3 +1606,16 @@ Widget dialogButton(String text, )); } } + +int get_version_num(String version) { + final list = version.split('.'); + var n = 0; + for (var i = 0; i < list.length; i++) { + n = n * 1000 + (int.tryParse(list[i]) ?? 0); + } + return n; +} + +int version_cmp(String v1, String v2) { + return get_version_num(v1) - get_version_num(v2); +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 55a5bbaef..fb67154bc 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -33,10 +33,12 @@ class RemotePage extends StatefulWidget { Key? key, required this.id, required this.menubarState, + this.switchUuid, }) : super(key: key); final String id; final MenubarState menubarState; + final String? switchUuid; final SimpleWrapper?> _lastState = SimpleWrapper(null); FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; @@ -100,7 +102,10 @@ class _RemotePageState extends State showKBLayoutTypeChooserIfNeeded( _ffi.ffiModel.pi.platform, _ffi.dialogManager); }); - _ffi.start(widget.id); + _ffi.start( + widget.id, + switchUuid: widget.switchUuid, + ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); _ffi.dialogManager diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 198b2aea7..a3532d49a 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -64,6 +64,7 @@ class _ConnectionTabPageState extends State { key: ValueKey(peerId), id: peerId, menubarState: _menubarState, + switchUuid: params['switch_uuid'], ), )); _update_remote_count(); @@ -84,6 +85,7 @@ class _ConnectionTabPageState extends State { if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); final id = args['id']; + final switchUuid = args['switch_uuid']; window_on_top(windowId()); ConnectionTypeState.init(id); tabController.add(TabInfo( @@ -96,6 +98,7 @@ class _ConnectionTabPageState extends State { key: ValueKey(id), id: id, menubarState: _menubarState, + switchUuid: switchUuid, ), )); } else if (call.method == "onDestroy") { diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index fa367f488..8c8679e96 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -516,6 +516,15 @@ class _CmControlPanel extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + Offstage( + offstage: !client.fromSwitch, + child: buildButton(context, + color: Colors.purple, + onClick: () => handleSwitchBack(context), + icon: Icon(Icons.reply, color: Colors.white), + text: "Switch Sides", + textColor: Colors.white), + ), Offstage( offstage: !showElevation, child: buildButton(context, color: Colors.green[700], onClick: () { @@ -674,6 +683,10 @@ class _CmControlPanel extends StatelessWidget { windowManager.close(); } } + + void handleSwitchBack(BuildContext context) { + bind.cmSwitchBack(connId: client.id); + } } void checkClickTime(int id, Function() callback) async { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6a0fa9104..6ea372b1f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -509,6 +509,7 @@ class _RemoteMenubarState extends State { 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([ @@ -651,6 +652,18 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } + if (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, + )); + } } if (pi.version.isNotEmpty) { @@ -721,6 +734,7 @@ class _RemoteMenubarState extends State { 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 = [ MenuEntryRadios( text: translate('Ratio'), @@ -880,9 +894,7 @@ class _RemoteMenubarState extends State { final fpsSlider = Offstage( offstage: (await bind.mainIsUsingPublicServer() && direct != true) || - (await bind.versionToNumber( - v: widget.ffi.ffiModel.pi.version) < - await bind.versionToNumber(v: '1.2.0')), + version_cmp(peer_version, '1.2.0') < 0, child: Row( children: [ Obx((() => Slider( @@ -1391,16 +1403,33 @@ void showAuditDialog(String id, dialogManager) async { focusNode: focusNode, )), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: close, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showConfirmSwitchSidesDialog( + String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close) { + submit() async { + await bind.sessionSwitchSides(id: id); + closeConnection(id: id); + } + + return CustomAlertDialog( + title: Text(translate('Switch Sides')), + content: Column( + children: [ + Text(translate('Please confirm if you want to share your desktop?')), + ], + ), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 641165e67..061c3293f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -199,6 +199,16 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.setShowElevation(show); } else if (name == 'cancel_msgbox') { cancelMsgBox(evt, peerId); + } else if (name == 'switch_sides') { + final peer_id = evt['peer_id'].toString(); + final uuid = evt['uuid'].toString(); + Future.delayed(Duration.zero, () { + rustDeskWinManager.newRemoteDesktop(peer_id, switch_uuid: uuid); + }); + } else if (name == 'switch_back') { + final peer_id = evt['peer_id'].toString(); + await bind.sessionSwitchSides(id: peer_id); + closeConnection(id: peer_id); } }; } @@ -1289,7 +1299,9 @@ class FFI { /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. void start(String id, - {bool isFileTransfer = false, bool isPortForward = false}) { + {bool isFileTransfer = false, + bool isPortForward = false, + String? switchUuid}) { assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; @@ -1305,7 +1317,11 @@ class FFI { } // ignore: unused_local_variable final addRes = bind.sessionAddSync( - id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward); + id: id, + isFileTransfer: isFileTransfer, + isPortForward: isPortForward, + switchUuid: switchUuid ?? "", + ); final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index c36a54db6..176b1ba2d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -601,6 +601,7 @@ class Client { bool restart = false; bool recording = false; bool disconnected = false; + bool fromSwitch = false; RxBool hasUnreadChatMessage = false.obs; @@ -621,6 +622,7 @@ class Client { restart = json['restart']; recording = json['recording']; disconnected = json['disconnected']; + fromSwitch = json['from_switch']; } Map toJson() { @@ -638,6 +640,7 @@ class Client { data['restart'] = restart; data['recording'] = recording; data['disconnected'] = disconnected; + data['from_switch'] = fromSwitch; return data; } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index cf6d78cd2..5087538c5 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -40,9 +40,13 @@ class RustDeskMultiWindowManager { int? _fileTransferWindowId; int? _portForwardWindowId; - Future newRemoteDesktop(String remoteId) async { - final msg = - jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId}); + Future newRemoteDesktop(String remoteId, + {String? switch_uuid}) async { + final msg = jsonEncode({ + "type": WindowType.RemoteDesktop.index, + "id": remoteId, + "switch_uuid": switch_uuid ?? "" + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -208,7 +212,7 @@ class RustDeskMultiWindowManager { } /// Remove active window which has [`windowId`] - /// + /// /// [Availability] /// This function should only be called from main window. /// For other windows, please post a unregister(hide) event to main window handler: diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index f5910d963..12d698045 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -564,6 +564,17 @@ message ElevationRequest { } } +message SwitchSidesRequest { + bytes uuid = 1; +} + +message SwitchSidesResponse { + bytes uuid = 1; + LoginRequest lr = 2; +} + +message SwitchBack {} + message Misc { oneof union { ChatMessage chat_message = 4; @@ -582,6 +593,8 @@ message Misc { ElevationRequest elevation_request = 18; string elevation_response = 19; bool portable_service_running = 20; + SwitchSidesRequest switch_sides_request = 21; + SwitchBack switch_back = 22; } } @@ -606,5 +619,6 @@ message Message { Misc misc = 19; Cliprdr cliprdr = 20; MessageBox message_box = 21; + SwitchSidesResponse switch_sides_response = 22; } } diff --git a/src/client.rs b/src/client.rs index 493448c3b..e9b8edf39 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,5 @@ pub use async_trait::async_trait; +use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -909,6 +910,7 @@ pub struct LoginConfigHandler { pub force_relay: bool, pub direct: Option, pub received: bool, + switch_uuid: Option, } impl Deref for LoginConfigHandler { @@ -936,7 +938,7 @@ impl LoginConfigHandler { /// /// * `id` - id of peer /// * `conn_type` - Connection type enum. - pub fn initialize(&mut self, id: String, conn_type: ConnType) { + pub fn initialize(&mut self, id: String, conn_type: ConnType, switch_uuid: Option) { self.id = id; self.conn_type = conn_type; let config = self.load_config(); @@ -948,6 +950,7 @@ impl LoginConfigHandler { self.force_relay = !self.get_option("force-always-relay").is_empty(); self.direct = None; self.received = false; + self.switch_uuid = switch_uuid; } /// Check if the client should auto login. @@ -1784,6 +1787,14 @@ pub async fn handle_hash( interface: &impl Interface, peer: &mut Stream, ) { + lc.write().unwrap().hash = hash.clone(); + let uuid = lc.read().unwrap().switch_uuid.clone(); + if let Some(uuid) = uuid { + if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { + send_switch_login_request(lc.clone(), peer, uuid).await; + return; + } + } let mut password = lc.read().unwrap().password.clone(); if password.is_empty() { if !password_preset.is_empty() { @@ -1848,6 +1859,26 @@ pub async fn handle_login_from_ui( send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; } +async fn send_switch_login_request( + lc: Arc>, + peer: &mut Stream, + uuid: Uuid, +) { + let mut msg_out = Message::new(); + msg_out.set_switch_sides_response(SwitchSidesResponse { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + lr: hbb_common::protobuf::MessageField::some( + lc.read() + .unwrap() + .create_login_msg(vec![]) + .login_request() + .to_owned(), + ), + ..Default::default() + }); + allow_err!(peer.send(&msg_out).await); +} + /// Interface for client to send data and commands. #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index b15949041..ff6d6c004 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1111,6 +1111,10 @@ impl Remote { ); } } + Some(misc::Union::SwitchBack(_)) => { + #[cfg(feature = "flutter")] + self.handler.switch_back(&self.handler.id); + } _ => {} }, Some(message::Union::TestDelay(t)) => { diff --git a/src/flutter.rs b/src/flutter.rs index 1369b5646..0b8cef704 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,3 +1,12 @@ +use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; +use crate::{client::*, flutter_ffi::EventToUI}; +use bytes::Bytes; +use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; +use hbb_common::{ + bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, + ResultType, +}; +use serde_json::json; use std::{ collections::HashMap, ffi::CString, @@ -5,18 +14,6 @@ use std::{ sync::{Arc, RwLock}, }; -use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; - -use hbb_common::{ - bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, - ResultType, -}; -use serde_json::json; - -use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; - -use crate::{client::*, flutter_ffi::EventToUI}; - pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; @@ -366,7 +363,17 @@ impl InvokeUiSession for FlutterHandler { ("y", &display.y.to_string()), ("width", &display.width.to_string()), ("height", &display.height.to_string()), - ("cursor_embedded", &{if display.cursor_embedded {1} else {0}}.to_string()), + ( + "cursor_embedded", + &{ + if display.cursor_embedded { + 1 + } else { + 0 + } + } + .to_string(), + ), ], ); } @@ -382,6 +389,10 @@ impl InvokeUiSession for FlutterHandler { fn clipboard(&self, content: String) { self.push_event("clipboard", vec![("content", &content)]); } + + fn switch_back(&self, peer_id: &str) { + self.push_event("switch_back", [("peer_id", peer_id)].into()); + } } /// Create a new remote session with the given id. @@ -391,7 +402,12 @@ impl InvokeUiSession for FlutterHandler { /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. /// * `is_port_forward` - If the session is used for port forward. -pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { +pub fn session_add( + id: &str, + is_file_transfer: bool, + is_port_forward: bool, + switch_uuid: &str, +) -> ResultType<()> { let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); @@ -409,11 +425,17 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R ConnType::DEFAULT_CONN }; + let switch_uuid = if switch_uuid.is_empty() { + None + } else { + Some(switch_uuid.to_string()) + }; + session .lc .write() .unwrap() - .initialize(session_id, conn_type); + .initialize(session_id, conn_type, switch_uuid); if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); @@ -590,3 +612,17 @@ pub fn set_cur_session_id(id: String) { *CUR_SESSION_ID.write().unwrap() = id; } } + +pub fn switch_sides(peer_id: &str, uuid: &Bytes) { + if let Some(stream) = GLOBAL_EVENT_STREAM.read().unwrap().get(APP_TYPE_MAIN) { + if let Ok(uuid) = uuid::Uuid::from_slice(uuid.to_vec().as_ref()) { + let uuid = uuid.to_string(); + let data = HashMap::from([ + ("name", "switch_sides"), + ("peer_id", peer_id), + ("uuid", &uuid), + ]); + stream.add(serde_json::ser::to_string(&data).unwrap_or("".into())); + } + } +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ca6823aa5..874cb4d4d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -84,8 +84,9 @@ pub fn session_add_sync( id: String, is_file_transfer: bool, is_port_forward: bool, + switch_uuid: String, ) -> SyncReturn { - if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) { + if let Err(e) = session_add(&id, is_file_transfer, is_port_forward, &switch_uuid) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -504,6 +505,12 @@ pub fn session_elevate_with_logon(id: String, username: String, password: String } } +pub fn session_switch_sides(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.switch_sides(); + } +} + pub fn main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); @@ -1066,6 +1073,10 @@ pub fn cm_elevate_portable(conn_id: i32) { crate::ui_cm_interface::elevate_portable(conn_id); } +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(); @@ -1108,10 +1119,6 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } -pub fn version_to_number(v: String) -> i64 { - hbb_common::get_version_number(&v) -} - pub fn option_synced() -> bool { crate::ui_interface::option_synced() } diff --git a/src/ipc.rs b/src/ipc.rs index 9048db766..d74842d64 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -166,6 +166,7 @@ pub enum Data { file_transfer_enabled: bool, restart: bool, recording: bool, + from_switch: bool, }, ChatMessage { text: String, @@ -207,6 +208,7 @@ pub enum Data { Empty, Disconnected, DataPortableService(DataPortableService), + SwitchBack, } #[tokio::main(flavor = "current_thread")] diff --git a/src/lang/ca.rs b/src/lang/ca.rs index bbcea1347..72f55b44b 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 2f56b6da0..14e8a463d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "弱"), ("Medium", "中"), ("Strong", "强"), + ("Switch Sides", "反转访问方向"), + ("Please confirm if you want to share your desktop?", "请确认要让对方访问你的桌面?"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 8852d602c..e2935770c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 53ae46bd4..937990ea8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index dd05dcdd5..7394a4628 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Schwach"), ("Medium", "Mittel"), ("Strong", "Stark"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 955a3287d..839c69bbb 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 5ab59b946..88b0ba8e9 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Débil"), ("Medium", "Media"), ("Strong", "Fuerte"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index a257425f1..dfd76405e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6edec8477..9c9860fb2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 4b54ba8ad..6ec1152cd 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Αδύναμο"), ("Medium", "Μέτριο"), ("Strong", "Δυνατό"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 9e1a4d982..295104a67 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 65c30ec6d..5604a0c52 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index e3c1a1f0f..2e313e101 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Debole"), ("Medium", "Media"), ("Strong", "Forte"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6ebb11ef5..a280940c7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a6825b523..1cdf529ce 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 816eb370d..59d26135f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index df985cccf..ee4b45334 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index dba37b5da..66373a5e9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 31c9153f2..5a137f391 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index abe642d9c..a7e42e0e4 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Слабый"), ("Medium", "Средний"), ("Strong", "Стойкий"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 56d14652d..c735cb28c 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 3d2ad3be8..6a17cc906 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 165597e7e..ebb43f6b7 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 739d53570..d9463318d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 498131d0c..146e60f9a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index adb05c943..729932973 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 2b062c3f7..a78509e59 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index a4a179c86..483ee67e3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index cd9f270ec..459c517ff 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "弱"), ("Medium", "中"), ("Strong", "強"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index ff24baab7..ca99be12e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 6988efba7..53de4e67c 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index a7526c8b4..83ec5db55 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -40,6 +40,7 @@ lazy_static::lazy_static! { static ref LOGIN_FAILURES: Arc::>> = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); + pub static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); } pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); @@ -102,6 +103,7 @@ pub struct Connection { chat_unanswered: bool, close_manually: bool, elevation_requested: bool, + from_switch: bool, } impl Subscriber for ConnInner { @@ -134,6 +136,7 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); +const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(30); impl Connection { pub async fn start( @@ -198,6 +201,7 @@ impl Connection { chat_unanswered: false, close_manually: false, elevation_requested: false, + from_switch: false, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -362,6 +366,13 @@ impl Connection { log::error!("Failed to start portable service from cm:{:?}", e); } } + ipc::Data::SwitchBack => { + let mut misc = Misc::new(); + misc.set_switch_back(SwitchBack::default()); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.send(msg).await; + } _ => {} } }, @@ -954,6 +965,7 @@ impl Connection { file_transfer_enabled: self.file_transfer_enabled(), restart: self.restart, recording: self.recording, + from_switch: self.from_switch, }); } @@ -1078,29 +1090,33 @@ impl Connection { return Config::get_option(enable_prefix_option).is_empty(); } - async fn on_message(&mut self, msg: Message) -> bool { - if let Some(message::Union::LoginRequest(lr)) = msg.union { - self.lr = lr.clone(); - if let Some(o) = lr.option.as_ref() { - self.update_option(o).await; - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::State(q), - ); - } else { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::DisableHwIfNotExist, - ); - } + 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; + if let Some(q) = o.video_codec_state.clone().take() { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::State(q), + ); } else { scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::DisableHwIfNotExist, ); } - self.video_ack_required = lr.video_ack_required; + } else { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::DisableHwIfNotExist, + ); + } + self.video_ack_required = lr.video_ack_required; + } + + async fn on_message(&mut self, msg: Message) -> bool { + if let Some(message::Union::LoginRequest(lr)) = msg.union { + self.handle_login_request_without_validation(&lr).await; if self.authorized { return true; } @@ -1247,6 +1263,21 @@ impl Connection { .unwrap() .update_network_delay(new_delay); } + } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { + #[cfg(feature = "flutter")] + if let Some(lr) = _s.lr.clone().take() { + self.handle_login_request_without_validation(&lr).await; + let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); + if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { + if let Some((instant, uuid_old)) = uuid_old { + if instant.elapsed() < SWITCH_SIDES_TIMEOUT && uuid == uuid_old { + self.from_switch = true; + self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); + self.send_logon_response().await; + } + } + } + } } else if self.authorized { match msg.union { Some(message::Union::MouseEvent(me)) => { @@ -1536,6 +1567,11 @@ impl Connection { } _ => {} }, + #[cfg(feature = "flutter")] + Some(misc::Union::SwitchSidesRequest(s)) => { + crate::flutter::switch_sides(&self.lr.my_id, &s.uuid); + return false; + } _ => {} }, _ => {} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1f3d5f7ec..21504d20d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -264,6 +264,8 @@ impl InvokeUiSession for SciterHandler { fn update_block_input_state(&self, on: bool) { self.call("updateBlockInputState", &make_args!(on)); } + + fn switch_back(&self, _id: &str) {} } pub struct SciterSession(Session); @@ -440,7 +442,7 @@ impl SciterSession { ConnType::DEFAULT_CONN }; - session.lc.write().unwrap().initialize(id, conn_type); + session.lc.write().unwrap().initialize(id, conn_type, None); Self(session) } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 551352ff7..dd0ce2b24 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -48,6 +48,7 @@ pub struct Client { pub file: bool, pub restart: bool, pub recording: bool, + pub from_switch: bool, #[serde(skip)] tx: UnboundedSender, } @@ -118,6 +119,7 @@ impl ConnectionManager { file: bool, restart: bool, recording: bool, + from_switch: bool, tx: mpsc::UnboundedSender, ) { let client = Client { @@ -134,6 +136,7 @@ impl ConnectionManager { file, restart, recording, + from_switch, tx, }; CLIENTS @@ -241,6 +244,14 @@ pub fn get_clients_length() -> usize { clients.len() } +#[inline] +#[cfg(feature = "flutter")] +pub fn switch_back(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::SwitchBack)); + }; +} + impl IpcTaskRunner { #[cfg(windows)] async fn enable_cliprdr_file_context(&mut self, conn_id: i32, enabled: bool) { @@ -308,9 +319,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording} => { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, from_switch,self.tx.clone()); self.authorized = authorized; self.conn_id = id; #[cfg(windows)] @@ -498,6 +509,7 @@ pub async fn start_listen( file, restart, recording, + from_switch, .. }) => { current_id = id; @@ -514,6 +526,7 @@ pub async fn start_listen( file, restart, recording, + from_switch, tx.clone(), ); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 00f1f90cf..800ca35c6 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -8,6 +8,7 @@ use crate::common::{self, is_keyboard_mode_supported, GrabState}; use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; +use bytes::Bytes; use hbb_common::config::{Config, LocalConfig, PeerConfig}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; @@ -18,6 +19,7 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; +use uuid::Uuid; pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -616,6 +618,22 @@ impl Session { pub fn elevate_with_logon(&self, username: String, password: String) { self.send(Data::ElevateWithLogon(username, password)); } + + pub fn switch_sides(&self) { + let uuid = Uuid::new_v4(); + crate::server::SWITCH_SIDES_UUID + .lock() + .unwrap() + .insert(self.id.clone(), (tokio::time::Instant::now(), uuid.clone())); + let mut misc = Misc::new(); + misc.set_switch_sides_request(SwitchSidesRequest { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -655,6 +673,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); + fn switch_back(&self, id: &str); } impl Deref for Session { From 81a60725f4969ea1fb376b06c20aa2e32576d38f Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Jan 2023 16:13:44 +0800 Subject: [PATCH 182/734] switch sides: remove outdate uuid Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 3 ++- src/server/connection.rs | 18 +++++++++++++++--- src/ui_session_interface.rs | 5 +---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6ea372b1f..62289d5f0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -652,7 +652,8 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } - if (version_cmp(peer_version, '1.2.0') >= 0) { + if (pi.platform != kPeerPlatformAndroid && + version_cmp(peer_version, '1.2.0') >= 0) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Switch Sides'), diff --git a/src/server/connection.rs b/src/server/connection.rs index 83ec5db55..e60d4652c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -40,7 +40,7 @@ lazy_static::lazy_static! { static ref LOGIN_FAILURES: Arc::>> = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); - pub static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); + static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); } pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); @@ -136,7 +136,7 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); -const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(30); +const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(10); impl Connection { pub async fn start( @@ -1267,10 +1267,14 @@ impl Connection { #[cfg(feature = "flutter")] if let Some(lr) = _s.lr.clone().take() { self.handle_login_request_without_validation(&lr).await; + SWITCH_SIDES_UUID + .lock() + .unwrap() + .retain(|_, v| v.0.elapsed() < SWITCH_SIDES_TIMEOUT); let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { if let Some((instant, uuid_old)) = uuid_old { - if instant.elapsed() < SWITCH_SIDES_TIMEOUT && uuid == uuid_old { + if uuid == uuid_old { self.from_switch = true; self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); self.send_logon_response().await; @@ -1792,6 +1796,14 @@ impl Connection { } } +#[cfg(feature = "flutter")] +pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { + SWITCH_SIDES_UUID + .lock() + .unwrap() + .insert(id, (tokio::time::Instant::now(), uuid)); +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 800ca35c6..a5f55a05d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -621,10 +621,7 @@ impl Session { pub fn switch_sides(&self) { let uuid = Uuid::new_v4(); - crate::server::SWITCH_SIDES_UUID - .lock() - .unwrap() - .insert(self.id.clone(), (tokio::time::Instant::now(), uuid.clone())); + crate::server::insert_switch_sides_uuid(self.id.clone(), uuid.clone()); let mut misc = Misc::new(); misc.set_switch_sides_request(SwitchSidesRequest { uuid: Bytes::from(uuid.as_bytes().to_vec()), From e57949d47204ac638ee5bc75679405e6b53c4fc9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Jan 2023 20:16:36 +0800 Subject: [PATCH 183/734] switch sides: use ipc to pass msg from ui to server Signed-off-by: 21pages --- src/ipc.rs | 12 +++++++++- src/server/connection.rs | 3 +-- src/ui_cm_interface.rs | 2 +- src/ui_session_interface.rs | 44 +++++++++++++++++++++++++++---------- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/ipc.rs b/src/ipc.rs index d74842d64..d4d803aec 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -208,7 +208,8 @@ pub enum Data { Empty, Disconnected, DataPortableService(DataPortableService), - SwitchBack, + SwitchSidesRequest(String), + SwitchSidesBack, } #[tokio::main(flavor = "current_thread")] @@ -429,6 +430,15 @@ async fn handle(data: Data, stream: &mut Connection) { Data::TestRendezvousServer => { crate::test_rendezvous_server(); } + Data::SwitchSidesRequest(id) => { + let uuid = uuid::Uuid::new_v4(); + crate::server::insert_switch_sides_uuid(id, uuid.clone()); + allow_err!( + stream + .send(&Data::SwitchSidesRequest(uuid.to_string())) + .await + ); + } _ => {} } } diff --git a/src/server/connection.rs b/src/server/connection.rs index e60d4652c..e4ab1b22e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -366,7 +366,7 @@ impl Connection { log::error!("Failed to start portable service from cm:{:?}", e); } } - ipc::Data::SwitchBack => { + ipc::Data::SwitchSidesBack => { let mut misc = Misc::new(); misc.set_switch_back(SwitchBack::default()); let mut msg = Message::new(); @@ -1796,7 +1796,6 @@ impl Connection { } } -#[cfg(feature = "flutter")] pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { SWITCH_SIDES_UUID .lock() diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index dd0ce2b24..ea3553c8a 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -248,7 +248,7 @@ pub fn get_clients_length() -> usize { #[cfg(feature = "flutter")] pub fn switch_back(id: i32) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { - allow_err!(client.tx.send(Data::SwitchBack)); + allow_err!(client.tx.send(Data::SwitchSidesBack)); }; } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a5f55a05d..a16327d75 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -17,6 +17,7 @@ use hbb_common::{fs, get_version_number, log, Stream}; use rdev::{Event, EventType::*}; 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 uuid::Uuid; @@ -619,17 +620,38 @@ impl Session { self.send(Data::ElevateWithLogon(username, password)); } - pub fn switch_sides(&self) { - let uuid = Uuid::new_v4(); - crate::server::insert_switch_sides_uuid(self.id.clone(), uuid.clone()); - let mut misc = Misc::new(); - misc.set_switch_sides_request(SwitchSidesRequest { - uuid: Bytes::from(uuid.as_bytes().to_vec()), - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send(Data::Message(msg_out)); + #[tokio::main(flavor = "current_thread")] + pub async fn switch_sides(&self) { + match crate::ipc::connect(1000, "").await { + Ok(mut conn) => { + if conn + .send(&crate::ipc::Data::SwitchSidesRequest(self.id.to_string())) + .await + .is_ok() + { + if let Ok(Some(data)) = conn.next_timeout(1000).await { + match data { + crate::ipc::Data::SwitchSidesRequest(str_uuid) => { + if let Ok(uuid) = Uuid::from_str(&str_uuid) { + let mut misc = Misc::new(); + misc.set_switch_sides_request(SwitchSidesRequest { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + } + _ => {} + } + } + } + } + Err(err) => { + log::info!("server not started (will try to start): {}", err); + } + } } } From c25796e44d1b880b56eea6c3e20f5becbd573512 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Jan 2023 21:43:39 +0800 Subject: [PATCH 184/734] switch sides: windows Signed-off-by: 21pages --- flutter/lib/common.dart | 4 +++- flutter/lib/models/model.dart | 6 ------ src/core_main.rs | 15 ++++++++++++++- src/flutter.rs | 14 -------------- src/server/connection.rs | 12 ++++++++++-- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index accbdf8df..13d0f2e60 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1307,8 +1307,10 @@ bool callUniLinksUriHandler(Uri uri) { // new connection if (uri.authority == "connection" && uri.path.startsWith("/new/")) { final peerId = uri.path.substring("/new/".length); + var param = uri.queryParameters; + String? switch_uuid = param["switch_uuid"]; Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(peerId); + rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid); }); return true; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 061c3293f..95ecde6e9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -199,12 +199,6 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.setShowElevation(show); } else if (name == 'cancel_msgbox') { cancelMsgBox(evt, peerId); - } else if (name == 'switch_sides') { - final peer_id = evt['peer_id'].toString(); - final uuid = evt['uuid'].toString(); - Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(peer_id, switch_uuid: uuid); - }); } else if (name == 'switch_back') { final peer_id = evt['peer_id'].toString(); await bind.sessionSwitchSides(id: peer_id); diff --git a/src/core_main.rs b/src/core_main.rs index 9083efe0e..76795576e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -298,6 +298,13 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option Option { - crate::flutter::switch_sides(&self.lr.my_id, &s.uuid); - return false; + if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { + crate::run_me(vec![ + "--connect", + &self.lr.my_id, + "--switch_uuid", + uuid.to_string().as_ref(), + ]) + .ok(); + return false; + } } _ => {} }, From b7844d11752f72b58a33e03e4e05419b32456ef9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 18 Jan 2023 13:54:56 +0800 Subject: [PATCH 185/734] switch sides: linux dbus use uri as para like uni_links Signed-off-by: 21pages --- flutter/lib/common.dart | 15 ++++++++++----- .../lib/desktop/pages/desktop_home_page.dart | 4 ++-- flutter/lib/models/model.dart | 10 +++------- src/core_main.rs | 18 ++++++++++-------- src/flutter.rs | 1 - src/platform/linux.rs | 14 ++++++++------ src/server/connection.rs | 6 ++++-- src/server/dbus.rs | 15 ++++++++------- 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 13d0f2e60..60e2246f5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1261,23 +1261,28 @@ StreamSubscription? listenUniLinks() { /// Returns true if we successfully handle the startup arguments. bool checkArguments() { + // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args final connectIndex = bootArgs.indexOf("--connect"); if (connectIndex == -1) { return false; } - String? arg = + String? id = bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1]; - if (arg != null) { - if (arg.startsWith(kUniLinksPrefix)) { - return parseRustdeskUri(arg); + final switchUuidIndex = bootArgs.indexOf("--switch_uuid"); + String? switchUuid = bootArgs.length < switchUuidIndex + 1 + ? null + : bootArgs[switchUuidIndex + 1]; + if (id != null) { + if (id.startsWith(kUniLinksPrefix)) { + return parseRustdeskUri(id); } else { // remove "--connect xxx" in the `bootArgs` array bootArgs.removeAt(connectIndex); bootArgs.removeAt(connectIndex); // fallback to peer id Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(arg); + rustDeskWinManager.newRemoteDesktop(id, switch_uuid: switchUuid); }); return true; } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 2773a3049..0501c298a 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -544,7 +544,7 @@ void setPasswordDialog() async { final p1 = TextEditingController(text: pw); var errMsg0 = ""; var errMsg1 = ""; - final RxString rxPass = p0.text.obs; + final RxString rxPass = pw.trim().obs; final rules = [ DigitValidationRule(), UppercaseValidationRule(), @@ -603,7 +603,7 @@ void setPasswordDialog() async { controller: p0, focusNode: FocusNode()..requestFocus(), onChanged: (value) { - rxPass.value = value; + rxPass.value = value.trim(); }, ), ), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 95ecde6e9..cf7a88312 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -184,13 +184,9 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_privacy_mode') { updatePrivacyMode(evt, peerId); } else if (name == 'new_connection') { - var arg = evt['peer_id'].toString(); - if (arg.startsWith(kUniLinksPrefix)) { - parseRustdeskUri(arg); - } else { - Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(arg); - }); + var uni_links = evt['uni_links'].toString(); + if (uni_links.startsWith(kUniLinksPrefix)) { + parseRustdeskUri(uni_links); } } else if (name == 'alias') { handleAliasChanged(evt); diff --git a/src/core_main.rs b/src/core_main.rs index 76795576e..75b5951da 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -305,11 +305,20 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { return None; } @@ -322,14 +331,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option>, chat_unanswered: bool, close_manually: bool, + #[allow(unused)] elevation_requested: bool, from_switch: bool, } @@ -1547,7 +1548,7 @@ impl Connection { self.send(msg).await; } } - Some(elevation_request::Union::Logon(r)) => { + Some(elevation_request::Union::Logon(_r)) => { #[cfg(windows)] { let mut err = "No need to elevate".to_string(); @@ -1556,7 +1557,8 @@ impl Connection { { use crate::portable_service::client; err = client::start_portable_service(client::StartPara::Logon( - r.username, r.password, + _r.username, + _r.password, )) .err() .map_or("".to_string(), |e| e.to_string()); diff --git a/src/server/dbus.rs b/src/server/dbus.rs index 5a38fe7cb..081db3e8f 100644 --- a/src/server/dbus.rs +++ b/src/server/dbus.rs @@ -5,10 +5,10 @@ /// [Flutter]: handle uni links for linux use dbus::blocking::Connection; use dbus_crossroads::{Crossroads, IfaceBuilder}; -use hbb_common::{log}; -use std::{error::Error, fmt, time::Duration}; +use hbb_common::log; #[cfg(feature = "flutter")] use std::collections::HashMap; +use std::{error::Error, fmt, time::Duration}; const DBUS_NAME: &str = "org.rustdesk.rustdesk"; const DBUS_PREFIX: &str = "/dbus"; @@ -30,15 +30,16 @@ impl fmt::Display for DbusError { impl Error for DbusError {} /// invoke new connection from dbus -/// +/// /// [Tips]: /// How to test by CLI: /// - use dbus-send command: /// `dbus-send --session --print-reply --dest=org.rustdesk.rustdesk /dbus org.rustdesk.rustdesk.NewConnection string:'PEER_ID'` -pub fn invoke_new_connection(peer_id: String) -> Result<(), Box> { +pub fn invoke_new_connection(uni_links: String) -> Result<(), Box> { let conn = Connection::new_session()?; let proxy = conn.with_proxy(DBUS_NAME, DBUS_PREFIX, DBUS_TIMEOUT); - let (ret,): (String,) = proxy.method_call(DBUS_NAME, DBUS_METHOD_NEW_CONNECTION, (peer_id,))?; + let (ret,): (String,) = + proxy.method_call(DBUS_NAME, DBUS_METHOD_NEW_CONNECTION, (uni_links,))?; if ret != DBUS_METHOD_RETURN_SUCCESS { log::error!("error on call new connection to dbus server"); return Err(Box::new(DbusError("not success".to_string()))); @@ -67,7 +68,7 @@ fn handle_client_message(builder: &mut IfaceBuilder<()>) { DBUS_METHOD_NEW_CONNECTION, (DBUS_METHOD_NEW_CONNECTION_ID,), (DBUS_METHOD_RETURN,), - move |_, _, (_peer_id,): (String,)| { + move |_, _, (_uni_links,): (String,)| { #[cfg(feature = "flutter")] { use crate::flutter::{self, APP_TYPE_MAIN}; @@ -79,7 +80,7 @@ fn handle_client_message(builder: &mut IfaceBuilder<()>) { { let data = HashMap::from([ ("name", "new_connection"), - ("peer_id", _peer_id.as_str()) + ("uni_links", _uni_links.as_str()), ]); if !stream.add(serde_json::ser::to_string(&data).unwrap_or("".to_string())) { log::error!("failed to add dbus message to flutter global dbus stream."); From ee0e84be37bf3a2b0d8f0ff45e5620b4b7512b79 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 19 Jan 2023 21:21:28 +0800 Subject: [PATCH 186/734] update flutter_rust_bridge to latest Signed-off-by: 21pages --- .github/workflows/flutter-ci.yml | 24 +---- .github/workflows/flutter-nightly.yml | 24 +---- .gitignore | 1 + Cargo.lock | 121 ++++++++++++++++++-------- Cargo.toml | 6 +- build.rs | 23 +++-- flutter/lib/common.dart | 11 +-- flutter/lib/models/model.dart | 4 +- flutter/pubspec.yaml | 7 +- src/flutter_ffi.rs | 4 + 10 files changed, 122 insertions(+), 103 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 738b57c4d..4e98f311d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -70,12 +70,7 @@ jobs: - name: Install flutter rust bridge deps run: | - dart pub global activate ffigen --version 5.0.1 - $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe - Push-Location .. - git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 - Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location - Pop-Location + cargo install flutter_rust_bridge_codegen Push-Location flutter ; flutter pub get ; Pop-Location ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -134,14 +129,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp - wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz - tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz - mkdir -p ~/.cargo/bin - mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen - popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -307,15 +295,10 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Install ffigen - run: | - dart pub global activate ffigen --version 5.0.1 - - name: Install flutter rust bridge deps shell: bash run: | - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd - name: Run flutter rust bridge @@ -328,6 +311,7 @@ jobs: name: bridge-artifact path: | ./src/bridge_generated.rs + ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index efa085ea8..145eee55d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -72,12 +72,7 @@ jobs: - name: Install flutter rust bridge deps run: | - dart pub global activate ffigen --version 5.0.1 - $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe - Push-Location .. - git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 - Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location - Pop-Location + cargo install flutter_rust_bridge_codegen Push-Location flutter ; flutter pub get ; Pop-Location ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -213,14 +208,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp - wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz - tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz - mkdir -p ~/.cargo/bin - mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen - popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -414,15 +402,10 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Install ffigen - run: | - dart pub global activate ffigen --version 5.0.1 - - name: Install flutter rust bridge deps shell: bash run: | - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd - name: Run flutter rust bridge @@ -435,6 +418,7 @@ jobs: name: bridge-artifact path: | ./src/bridge_generated.rs + ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart diff --git a/.gitignore b/.gitignore index 1ecea7af8..fd5b5955e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ cert.pfx sciter.dll **pdb src/bridge_generated.rs +src/bridge_generated.io.rs *deb rustdesk *.cache diff --git a/Cargo.lock b/Cargo.lock index 8dfde0335..693ae7d4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,11 +45,13 @@ dependencies = [ [[package]] name = "allo-isolate" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb993621e6bf1b67591005b0adad126159a0ab31af379743906158aed5330d0" +checksum = "8ed55848be9f41d44c79df6045b680a74a78bc579e0813f7f196cd7928e22fb1" dependencies = [ + "anyhow", "atomic", + "chrono", ] [[package]] @@ -471,6 +473,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + [[package]] name = "bumpalo" version = "3.11.1" @@ -565,9 +573,9 @@ dependencies = [ [[package]] name = "cbindgen" -version = "0.23.0" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900" +checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" dependencies = [ "clap 3.2.23", "heck 0.4.0", @@ -838,6 +846,16 @@ dependencies = [ "toml", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + [[package]] name = "convert_case" version = "0.5.0" @@ -1335,6 +1353,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "delegate" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1672,6 +1701,18 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "extend" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5216e387a76eebaaf11f6d871ec8a4aae0b25f05456ee21f228e024b1b3610" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "failure" version = "0.1.8" @@ -1744,28 +1785,44 @@ dependencies = [ [[package]] name = "flutter_rust_bridge" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8079119bbe8fb63d7ebb731fa2aa68c6c8375f4ac95ca26d5868e64c0f4b9244" dependencies = [ "allo-isolate", "anyhow", + "build-target", + "bytemuck", + "cc", + "chrono", + "console_error_panic_hook", "flutter_rust_bridge_macros", + "js-sys", "lazy_static", + "libc", + "log", "parking_lot 0.12.1", "threadpool", + "wasm-bindgen", + "web-sys", ] [[package]] name = "flutter_rust_bridge_codegen" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd7396bc479eae8aa24243e4c0e3d3dbda1909134f8de6bde4f080d262c9a0d" dependencies = [ "anyhow", "cargo_metadata", "cbindgen", + "clap 3.2.23", "convert_case", + "delegate", "enum_dispatch", "env_logger 0.9.3", + "extend", + "itertools 0.10.5", "lazy_static", "log", "pathdiff", @@ -1773,17 +1830,18 @@ dependencies = [ "regex", "serde 1.0.149", "serde_yaml", - "structopt", "syn", "tempfile", "thiserror", "toml", + "topological-sort", ] [[package]] name = "flutter_rust_bridge_macros" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5cd827645690ef378be57a890d0581e17c28d07b712872af7d744f454fd27d" [[package]] name = "fnv" @@ -2164,7 +2222,7 @@ checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" dependencies = [ "anyhow", "heck 0.3.3", - "itertools", + "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", "proc-macro2", @@ -2798,6 +2856,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.3.4" @@ -5150,30 +5217,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap 2.34.0", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "strum" version = "0.18.0" @@ -5564,6 +5607,12 @@ dependencies = [ "serde 1.0.149", ] +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "tower-service" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 427fcd4e7..1e9af30e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" -flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } +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"] } @@ -126,7 +126,7 @@ android_logger = "0.11" jni = "0.19" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } +flutter_rust_bridge = "1.61.1" [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] @@ -144,7 +144,7 @@ winapi = { version = "0.3", features = [ "winnt" ] } cc = "1.0" hbb_common = { path = "libs/hbb_common" } simple_rc = { path = "libs/simple_rc", optional = true } -flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } +flutter_rust_bridge_codegen = "1.61.1" [dev-dependencies] hound = "3.5" diff --git a/build.rs b/build.rs index ade63f0bc..d15f27424 100644 --- a/build.rs +++ b/build.rs @@ -85,26 +85,35 @@ fn install_oboe() { #[cfg(feature = "flutter")] fn gen_flutter_rust_bridge() { + use lib_flutter_rust_bridge_codegen::{ + config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts, + }; let llvm_path = match std::env::var("LLVM_HOME") { Ok(path) => Some(vec![path]), Err(_) => None, }; // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); - // settings for fbr_codegen - let opts = lib_flutter_rust_bridge_codegen::Opts { + // Options for frb_codegen + let raw_opts = RawOpts { // Path of input Rust code - rust_input: "src/flutter_ffi.rs".to_string(), + rust_input: vec!["src/flutter_ffi.rs".to_string()], // Path of output generated Dart code - dart_output: "flutter/lib/generated_bridge.dart".to_string(), + dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()], // Path of output generated C header c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]), - // for other options lets use default + /// Path to the installed LLVM llvm_path, + // for other options use defaults ..Default::default() }; - // run fbr_codegen - lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap(); + // get opts from raw opts + let configs = config_parse(raw_opts); + // generation of rust api for ffi + let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap(); + for config in configs.iter() { + frb_codegen(config, &all_symbols).unwrap(); + } } fn main() { diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 60e2246f5..b7bf2b30e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1614,15 +1614,6 @@ Widget dialogButton(String text, } } -int get_version_num(String version) { - final list = version.split('.'); - var n = 0; - for (var i = 0; i < list.length; i++) { - n = n * 1000 + (int.tryParse(list[i]) ?? 0); - } - return n; -} - int version_cmp(String v1, String v2) { - return get_version_num(v1) - get_version_num(v2); + return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index cf7a88312..986d93fe8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1316,14 +1316,14 @@ class FFI { final cb = ffiModel.startEventListener(id); () async { await for (final message in stream) { - if (message is Event) { + if (message is EventToUI_Event) { try { Map event = json.decode(message.field0); await cb(event); } catch (e) { debugPrint('json.decode fail1(): $e, ${message.field0}'); } - } else if (message is Rgba) { + } else if (message is EventToUI_Rgba) { imageModel.onRgba(message.field0); } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f096218b0..a63f49ba8 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -51,11 +51,7 @@ dependencies: image_picker: ^0.8.5 image: ^3.1.3 back_button_interceptor: ^6.0.1 - flutter_rust_bridge: - git: - url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge - ref: master - path: frb_dart + flutter_rust_bridge: ^1.61.1 window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager @@ -103,6 +99,7 @@ dev_dependencies: 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:main icons_launcher: diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 874cb4d4d..5468f580c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1119,6 +1119,10 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } +pub fn version_to_number(v: String) -> SyncReturn { + SyncReturn(hbb_common::get_version_number(&v)) +} + pub fn option_synced() -> bool { crate::ui_interface::option_synced() } From 79461178eacc4fd31a198f9f1b178fb128b6b0eb Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 20 Jan 2023 00:08:18 -0700 Subject: [PATCH 187/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index efa085ea8..c7855c281 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -560,6 +560,8 @@ jobs: popd mkdir -p signed-apk; pushd signed-apk mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + pwd + ls - uses: r0adkll/sign-android-release@v1 name: Sign app APK @@ -599,6 +601,9 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: | ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + run: | + pwd + ls build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From bbb853de9fa75ddac2215bd3b2ad24e156811783 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 20 Jan 2023 00:09:29 -0700 Subject: [PATCH 188/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index c7855c281..b7ccb08bc 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -600,10 +600,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - run: | - pwd - ls + ./rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From 05f9a2ccf84135e9f7170fae24f248b75d504f54 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 20 Jan 2023 00:35:04 -0700 Subject: [PATCH 189/734] /signed-apk dir --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b7ccb08bc..571f1eb34 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -600,7 +600,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - ./rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From 9c369e5f498b138d7ce4b5953d49a978b5511f88 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 20 Jan 2023 00:59:27 -0700 Subject: [PATCH 190/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 571f1eb34..57c04e8ff 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -560,8 +560,6 @@ jobs: popd mkdir -p signed-apk; pushd signed-apk mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . - pwd - ls - uses: r0adkll/sign-android-release@v1 name: Sign app APK From 3dcada128b600a0c364e8e0ae5266f38c3cfa226 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 20 Jan 2023 21:03:30 +0800 Subject: [PATCH 191/734] fix cur session Signed-off-by: fufesou --- src/flutter.rs | 2 +- src/flutter_ffi.rs | 1 - src/keyboard.rs | 87 ++++++++++++++++++++++--------------- src/ui.rs | 18 +++++--- src/ui_interface.rs | 1 + src/ui_session_interface.rs | 6 +-- 6 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 1369b5646..f4e2b8363 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -23,7 +23,7 @@ pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; pub(super) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; lazy_static::lazy_static! { - static ref CUR_SESSION_ID: RwLock = Default::default(); + 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 } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ca6823aa5..d92cfba23 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -319,7 +319,6 @@ pub fn session_enter_or_leave(id: String, enter: bool) { #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(session) = SESSIONS.read().unwrap().get(&id) { if enter { - crate::keyboard::set_cur_session(session.clone()); session.enter(); } else { session.leave(); diff --git a/src/keyboard.rs b/src/keyboard.rs index 183154ca0..de1abd231 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -2,10 +2,9 @@ use crate::client::get_key_state; use crate::common::GrabState; #[cfg(feature = "flutter")] -use crate::flutter::FlutterHandler; +use crate::flutter::{CUR_SESSION_ID, SESSIONS}; #[cfg(not(any(feature = "flutter", feature = "cli")))] -use crate::ui::remote::SciterHandler; -use crate::ui_session_interface::Session; +use crate::ui::CUR_SESSION; use hbb_common::{log, message_proto::*}; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] @@ -22,16 +21,6 @@ static mut IS_ALT_GR: bool = false; #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -#[cfg(feature = "flutter")] -lazy_static::lazy_static! { - static ref CUR_SESSION: Arc>>> = Default::default(); -} - -#[cfg(not(any(feature = "flutter", feature = "cli")))] -lazy_static::lazy_static! { - static ref CUR_SESSION: Arc>>> = Default::default(); -} - lazy_static::lazy_static! { static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); static ref MODIFIERS_STATE: Mutex> = { @@ -48,23 +37,21 @@ lazy_static::lazy_static! { }; } -#[cfg(feature = "flutter")] -pub fn set_cur_session(session: Session) { - *CUR_SESSION.lock().unwrap() = Some(session); -} - -#[cfg(not(any(feature = "flutter", feature = "cli")))] -pub fn set_cur_session(session: Session) { - *CUR_SESSION.lock().unwrap() = Some(session); -} - pub mod client { use super::*; pub fn get_keyboard_mode() -> String { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - return handler.get_keyboard_mode(); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + return session.get_keyboard_mode(); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + return session.get_keyboard_mode(); } "legacy".to_string() } @@ -164,15 +151,20 @@ pub mod client { } } - pub fn lock_screen() { + pub fn event_lock_screen() -> KeyEvent { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::LockScreen); key_event.down = true; key_event.mode = KeyboardMode::Legacy.into(); - send_key_event(&key_event); + key_event } - pub fn ctrl_alt_del() { + #[inline] + pub fn lock_screen() { + send_key_event(&event_lock_screen()); + } + + pub fn event_ctrl_alt_del() -> KeyEvent { let mut key_event = KeyEvent::new(); if get_peer_platform() == "Windows" { key_event.set_control_key(ControlKey::CtrlAltDel); @@ -183,7 +175,12 @@ pub mod client { key_event.press = true; } key_event.mode = KeyboardMode::Legacy.into(); - send_key_event(&key_event); + key_event + } + + #[inline] + pub fn ctrl_alt_del() { + send_key_event(&event_ctrl_alt_del()); } } @@ -254,6 +251,8 @@ pub fn release_remote_keys() { for key in to_release { let event_type = EventType::KeyRelease(key); let event = event_type_to_event(event_type); + // to-do: BUG + // Release events should be sent to the corresponding sessions, instead of current session. client::process_event(&event, None); } } @@ -382,16 +381,32 @@ pub fn event_type_to_event(event_type: EventType) -> Event { } pub fn send_key_event(key_event: &KeyEvent) { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.send_key_event(key_event); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + session.send_key_event(key_event); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + session.send_key_event(key_event); } } pub fn get_peer_platform() -> String { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - return handler.peer_platform(); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + return session.peer_platform(); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + return session.peer_platform(); } "Windows".to_string() } diff --git a/src/ui.rs b/src/ui.rs index 4cd9ce3f7..b8473072d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -13,9 +13,9 @@ use hbb_common::{ log, }; -use crate::common::get_app_name; -use crate::ipc; -use crate::ui_interface::*; +#[cfg(not(any(feature = "flutter", feature = "cli")))] +use crate::ui_session_interface::Session; +use crate::{common::get_app_name, ipc, ui_interface::*}; mod cm; #[cfg(feature = "inline")] @@ -35,6 +35,11 @@ lazy_static::lazy_static! { static ref STUPID_VALUES: Mutex>>> = Default::default(); } +#[cfg(not(any(feature = "flutter", feature = "cli")))] +lazy_static::lazy_static! { + pub static ref CUR_SESSION: Arc>>> = Default::default(); +} + struct UIHostHandler; pub fn start(args: &mut [String]) { @@ -119,9 +124,10 @@ pub fn start(args: &mut [String]) { frame.register_behavior("native-remote", move || { let handler = remote::SciterSession::new(cmd.clone(), id.clone(), pass.clone(), args.clone()); - #[cfg(not(feature = "flutter"))] - crate::keyboard::set_cur_session(handler.inner()); - + #[cfg(not(any(feature = "flutter", feature = "cli")))] + { + *CUR_SESSION.lock().unwrap() = Some(handler.inner()); + } Box::new(handler) }); page = "remote.html"; diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 2e6ef561c..ebaf8c317 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -577,6 +577,7 @@ pub fn is_installed_daemon(_prompt: bool) -> bool { } #[inline] +#[cfg(feature = "flutter")] pub fn is_can_input_monitoring(_prompt: bool) -> bool { #[cfg(target_os = "macos")] return crate::platform::macos::is_can_input_monitoring(_prompt); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 00f1f90cf..00d046882 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -4,7 +4,7 @@ use crate::client::{ input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -use crate::common::{self, is_keyboard_mode_supported, GrabState}; +use crate::common::{self, GrabState}; use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -780,10 +780,10 @@ impl Interface for Session { impl Session { pub fn lock_screen(&self) { - crate::keyboard::client::lock_screen(); + self.send_key_event(&crate::keyboard::client::event_lock_screen()); } pub fn ctrl_alt_del(&self) { - crate::keyboard::client::ctrl_alt_del(); + self.send_key_event(&crate::keyboard::client::event_ctrl_alt_del()); } } From efe9ba18cae424568f67e7004ad986453f47b422 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 21 Jan 2023 13:02:46 +0800 Subject: [PATCH 192/734] fix: --install cannot be invoke caused by singleton --- flutter/windows/runner/main.cpp | 49 ++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index d76b8c040..7437b0344 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -1,20 +1,21 @@ #include #include -#include #include +#include +#include + +#include #include #include "flutter_window.h" #include "utils.h" -// #include - -#include typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); const char* uniLinksPrefix = "rustdesk://"; +/// Note: `--server`, `--service` are already handled in [core_main.rs]. +const std::vector parameters_white_list = {"--install"}; -// auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { @@ -40,6 +41,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, } std::vector command_line_arguments = GetCommandLineArguments(); + // Remove possible trailing whitespace from command line arguments + for (auto& argument : command_line_arguments) { + argument.erase(argument.find_last_not_of(" \n\r\t")); + } int args_len = 0; char** c_args = rustdesk_core_main(&args_len); @@ -51,21 +56,33 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, std::vector rust_args(c_args, c_args + args_len); free_c_args(c_args, args_len); - // uni links dispatch + // Uri links dispatch HWND hwnd = ::FindWindow(_T("FLUTTER_RUNNER_WIN32_WINDOW"), _T("RustDesk")); if (hwnd != NULL) { - if (!command_line_arguments.empty()) { - // Dispatch command line arguments - DispatchToUniLinksDesktop(hwnd); - } else { - // Not called with arguments, or just open the app shortcut on desktop. - // So we just show the main window instead. - ::ShowWindow(hwnd, SW_NORMAL); - ::SetForegroundWindow(hwnd); + // Allow multiple flutter instances when being executed by parameters + // contained in whitelists. + bool allow_multiple_instances = false; + for (auto& whitelist_param : parameters_white_list) { + allow_multiple_instances = + allow_multiple_instances || + std::find(command_line_arguments.begin(), + command_line_arguments.end(), + whitelist_param) != command_line_arguments.end(); + } + if (!allow_multiple_instances) { + if (!command_line_arguments.empty()) { + // Dispatch command line arguments + DispatchToUniLinksDesktop(hwnd); + } else { + // Not called with arguments, or just open the app shortcut on desktop. + // So we just show the main window instead. + ::ShowWindow(hwnd, SW_NORMAL); + ::SetForegroundWindow(hwnd); + } + return EXIT_FAILURE; } - return EXIT_FAILURE; } - + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) From d493a6c27a026807fb1c674dde883a1fc33a12b3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 21 Jan 2023 13:16:07 +0800 Subject: [PATCH 193/734] opt: add --cm --- flutter/windows/runner/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 7437b0344..b7fa64dc0 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -14,7 +14,7 @@ typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); const char* uniLinksPrefix = "rustdesk://"; /// Note: `--server`, `--service` are already handled in [core_main.rs]. -const std::vector parameters_white_list = {"--install"}; +const std::vector parameters_white_list = {"--install", "--cm"}; int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) From 84a45ac48fe69b5c67f698e07ad54e3f99e5fe46 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 21 Jan 2023 18:02:04 +0800 Subject: [PATCH 194/734] hide switch sides menu until #2893 fixed Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 62289d5f0..227002645 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -652,7 +652,8 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } - if (pi.platform != kPeerPlatformAndroid && + if (false && + pi.platform != kPeerPlatformAndroid && version_cmp(peer_version, '1.2.0') >= 0) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( From 1a3f1d38fb2d049d33bb748b1f6b69291997d6a7 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Sat, 21 Jan 2023 13:51:33 +0100 Subject: [PATCH 195/734] Update it.rs --- src/lang/it.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 2e313e101..7b979aff0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Il servizio è in esecuzione"), ("Service is not running", "Il servizio non è in esecuzione"), ("not_ready_status", "Non pronto. Verifica la tua connessione"), - ("Control Remote Desktop", "Controlla una scrivania remota"), + ("Control Remote Desktop", "Controlla un desktop remoto"), ("Transfer File", "Trasferisci file"), ("Connect", "Connetti"), ("Recent Sessions", "Sessioni recenti"), @@ -372,7 +372,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Abilita il rilevamento della LAN"), ("Deny LAN Discovery", "Nega il rilevamento della LAN"), ("Write a message", "Scrivi un messaggio"), - ("Prompt", "Prompt"), + ("Prompt", "Richiede"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), ("elevated_foreground_window_tip", "La finestra corrente del desktop remoto richiede privilegi più elevati per funzionare, quindi non è in grado di utilizzare temporaneamente il mouse e la tastiera. È possibile chiedere all'utente remoto di ridurre a icona la finestra corrente o di fare clic sul pulsante di elevazione nella finestra di gestione della connessione. Per evitare questo problema, si consiglia di installare il software sul dispositivo remoto."), ("Disconnected", "Disconnesso"), @@ -423,15 +423,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Richiedi elevazione dei diritti"), ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), - ("uppercase", "maiuscolo"), - ("lowercase", "minuscolo"), - ("digit", "numero"), - ("special character", "carattere speciale"), - ("length>=8", "lunghezza >= 8"), + ("uppercase", "Maiuscola"), + ("lowercase", "Minuscola"), + ("digit", "Numero"), + ("special character", "Carattere speciale"), + ("length>=8", "Lunghezza >= 8"), ("Weak", "Debole"), ("Medium", "Media"), ("Strong", "Forte"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "Cambia lato"), + ("Please confirm if you want to share your desktop?", "Vuoi condividere il tuo desktop?"), ].iter().cloned().collect(); } From 761838495a819d7515375e6d15e51ccf8bf38d1d Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 21 Jan 2023 14:42:36 +0100 Subject: [PATCH 196/734] Update de.rs --- src/lang/de.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 7394a4628..a567877a2 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -145,7 +145,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Configure", "Konfigurieren"), ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."), ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), - ("Installing ...", "Installiere..."), + ("Installing ...", "Installieren..."), ("Install", "Installieren"), ("Installation", "Installation"), ("Installation Path", "Installationspfad"), @@ -201,7 +201,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("x11 expected", "X11 erwartet"), ("Port", "Port"), ("Settings", "Einstellungen"), - ("Username", " Benutzername"), + ("Username", "Benutzername"), ("Invalid port", "Ungültiger Port"), ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), @@ -431,7 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Schwach"), ("Medium", "Mittel"), ("Strong", "Stark"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "Seiten wechseln"), + ("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, ob Sie Ihren Desktop freigeben möchten."), ].iter().cloned().collect(); } From 3ee76fcf4f47ff2582788ea36a4fb6a061b43bee Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 21 Jan 2023 17:32:58 +0300 Subject: [PATCH 197/734] Update ru.rs --- src/lang/ru.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a7e42e0e4..8103ae3a3 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -431,7 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Слабый"), ("Medium", "Средний"), ("Strong", "Стойкий"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "Переключить стороны"), + ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ].iter().cloned().collect(); } From 490b4cbbb9ed711cce9179b6a1d5f55746fa3726 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Sat, 21 Jan 2023 18:51:01 +0100 Subject: [PATCH 198/734] 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 5161f9846..2b109c18f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -431,7 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Débil"), ("Medium", "Media"), ("Strong", "Fuerte"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "Intercambiar lados"), + ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), ].iter().cloned().collect(); } From 03b2546008688f6c259eef7b9161c46278db7384 Mon Sep 17 00:00:00 2001 From: Jimmy GALLAND Date: Sat, 21 Jan 2023 22:26:33 +0100 Subject: [PATCH 199/734] update FR --- src/lang/fr.rs | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9c9860fb2..ea2dbfede 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -14,8 +14,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is not running", "Le service ne fonctionne pas"), ("not_ready_status", "Pas prêt, veuillez vérifier la connexion réseau"), ("Control Remote Desktop", "Contrôler le bureau à distance"), - ("Transfer File", "Transférer le fichier"), - ("Connect", "Connecter"), + ("Transfer File", "Transfert de fichiers"), + ("Connect", "Se connecter"), ("Recent Sessions", "Sessions récentes"), ("Address Book", "Carnet d'adresses"), ("Confirmation", "Confirmation"), @@ -303,7 +303,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "en mode privé"), ("Out privacy mode", "hors mode de confidentialité"), ("Language", "Langue"), - ("Keep RustDesk background service", "Gardez le service Rustdesk service arrière plan"), + ("Keep RustDesk background service", "Gardez le service RustDesk service arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), ("Connection not allowed", "Connexion non autorisée"), @@ -412,26 +412,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Selectionner la disposition du clavier local"), ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), ("Always use software rendering", "Utiliser toujours le rendu logiciel"), - ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à Rustdesk l'autorisation \"Surveillance de l’entrée\"."), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), + ("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."), + ("Wait", "En cours"), + ("Elevation Error", "Erreur d'augmentation des privilèges"), + ("Ask the remote user for authentication", "Demander à l'utilisateur distant de s'authentifier"), + ("Choose this if the remote account is administrator", "Choisissez ceci si le compte distant est le compte d'administrateur"), + ("Transmit the username and password of administrator", "Transmettre le nom d'utilisateur et le mot de passe de l'administrateur"), + ("still_click_uac_tip", "Nécessite toujours que l'utilisateur distant confirme par la fenêtre UAC de RustDesk en cours d'éxécution."), + ("Request Elevation", "Demande d'augmentation des privilèges"), + ("wait_accept_uac_tip", "Veuillez attendre que l'utilisateur distant accepte la boîte de dialogue UAC."), + ("Elevate successfully", "Augmentation des privilèges avec succès"), + ("uppercase", "majuscule"), + ("lowercase", "minuscule"), + ("digit", "chiffre"), + ("special character", "caractère spécial"), + ("length>=8", "longueur>=8"), + ("Weak", "Faible"), + ("Medium", "Moyen"), + ("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 ?"), ].iter().cloned().collect(); } From b556b5d7f6935c4a027d9127dafc8a07fa61d1cc Mon Sep 17 00:00:00 2001 From: Jimmy GALLAND Date: Sat, 21 Jan 2023 23:07:48 +0100 Subject: [PATCH 200/734] add templates/translations for Tab Home label, and two other label translations in About tab --- 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/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/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 +++ 31 files changed, 93 insertions(+) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 72f55b44b..64b9bb35f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 14e8a463d..b95f79972 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "静音"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), ("Hardware Codec", "硬件编解码"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e2935770c..b40f79405 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Ztlumit"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Vstup zvuku"), ("Enhancements", ""), ("Hardware Codec", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 937990ea8..8bd3e9a7b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Sluk for mikrofonen"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Lydindgang"), ("Enhancements", ""), ("Hardware Codec", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 7394a4628..e1adc224b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), ("Hardware Codec", "Hardware-Codec"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 839c69bbb..cbaa013d5 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Pri"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Muta"), ("Audio Input", "Aŭdia enigo"), ("Enhancements", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 5161f9846..613497476 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Acerca de"), ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), ("Privacy Statement", "Declaración de privacidad"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index dfd76405e..a7a4df073 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "درباره"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "بستن صدا"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9c9860fb2..273760aac 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "À propos de"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), ("Privacy Statement", "Déclaration de confidentialité"), + ("Build Date", "Date de compilation"), + ("Version", "Version"), + ("Home", "Accueil"), ("Mute", "Muet"), ("Audio Input", "Entrée audio"), ("Enhancements", "Améliorations"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 6ec1152cd..b50f9fbf5 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Πληροφορίες"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Σίγαση"), ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 295104a67..c4e791da8 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Némítás"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), ("Hardware Codec", "Hardware kodek"), diff --git a/src/lang/id.rs b/src/lang/id.rs index 5604a0c52..9b8fb9f33 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Tentang"), ("Slogan_tip", ""), ("Privacy Statement", "Pernyataan Privasi"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Bisukan"), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 7b979aff0..e56893249 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a280940c7..04b199950 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "情報"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "ミュート"), ("Audio Input", "音声入力デバイス"), ("Enhancements", "追加機能"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1cdf529ce..a2d55bd1c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "정보"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "음소거"), ("Audio Input", "오디오 입력"), ("Enhancements", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 59d26135f..328f4c29b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Туралы"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Дыбыссыздандыру"), ("Audio Input", "Аудио Еңгізу"), ("Enhancements", "Жақсартулар"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index ee4b45334..061b97f99 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 66373a5e9..8ae67062c 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5a137f391..fbe2c1cf8 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a7e42e0e4..4f222291e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), ("Privacy Statement", "Заявление о конфиденциальности"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index c735cb28c..609523f05 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O RustDesk"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Stíšiť"), ("Audio Input", "Zvukový vstup"), ("Enhancements", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6a17cc906..c779ca33c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Izklopi zvok"), ("Audio Input", "Avdio vhod"), ("Enhancements", "Izboljšave"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ebb43f6b7..1939f6275 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Rreth"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Pa zë"), ("Audio Input", "Inputi zërit"), ("Enhancements", "Përmirësimet"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d9463318d..91c8f31b3 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Utišaj"), ("Audio Input", "Audio ulaz"), ("Enhancements", "Proširenja"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 146e60f9a..65bfc5122 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Om"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Tyst"), ("Audio Input", "Ljud input"), ("Enhancements", "Förbättringar"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 729932973..bd2d44d80 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", ""), ("Audio Input", ""), ("Enhancements", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index a78509e59..7e1d8c45a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "เกี่ยวกับ"), ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "ปิดเสียง"), ("Audio Input", "ออดิโออินพุท"), ("Enhancements", "การปรับปรุง"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 483ee67e3..47e59e551 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Hakkında"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Sustur"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 459c517ff..9404c1192 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "關於"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "靜音"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index ca99be12e..eadd7ed84 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Вимкнути звук"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 53de4e67c..c5d44ebc7 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "About"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Tắt tiếng"), ("Audio Input", "Đầu vào âm thanh"), ("Enhancements", "Các tiện itchs"), From 87a2705ba505223a7869f886911da6e09ca47298 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 22 Jan 2023 20:23:11 +0800 Subject: [PATCH 201/734] refactor, remove peer type Signed-off-by: fufesou --- flutter/lib/common/widgets/peer_card.dart | 135 ++++++++---------- .../lib/desktop/widgets/tabbar_widget.dart | 2 +- 2 files changed, 57 insertions(+), 80 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index c07b458bc..3feaef51d 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -316,21 +316,11 @@ class _PeerCardState extends State<_PeerCard> bool get wantKeepAlive => true; } -enum CardType { - recent, - fav, - lan, - ab, - grp, -} - abstract class BasePeerCard extends StatelessWidget { final Peer peer; final EdgeInsets? menuPadding; - final CardType cardType; - BasePeerCard( - {required this.peer, required this.cardType, this.menuPadding, Key? key}) + BasePeerCard({required this.peer, this.menuPadding, Key? key}) : super(key: key); @override @@ -435,7 +425,7 @@ abstract class BasePeerCard extends StatelessWidget { if (Navigator.canPop(context)) { Navigator.pop(context); } - _rdpDialog(id, cardType); + _rdpDialog(id); }, )), )) @@ -480,6 +470,12 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + Future _isForceAlwaysRelay(String id) async { + return (await bind.mainGetPeerOption(id: id, key: 'force-always-relay')) + .isNotEmpty; + } + @protected Future> _forceAlwaysRelayAction(String id) async { const option = 'force-always-relay'; @@ -487,16 +483,12 @@ abstract class BasePeerCard extends StatelessWidget { switchType: SwitchType.scheckbox, text: translate('Always connect via relay'), getter: () async { - if (cardType == CardType.ab) { - return gFFI.abModel.find(id)?.forceAlwaysRelay ?? false; - } else { - return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; - } + return await _isForceAlwaysRelay(id); }, setter: (bool v) async { gFFI.abModel.setPeerForceAlwaysRelay(id, v); await bind.mainSetPeerOption( - id: id, key: option, value: bool2option('force-always-relay', v)); + id: id, key: option, value: bool2option(option, v)); }, padding: menuPadding, dismissOnClicked: true, @@ -621,14 +613,13 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + Future _getAlias(String id) async => + await bind.mainGetPeerOption(id: id, key: 'alias'); + void _rename(String id) async { RxBool isInProgress = false.obs; - String name; - if (cardType == CardType.ab) { - name = gFFI.abModel.find(id)?.alias ?? ""; - } else { - name = await bind.mainGetPeerOption(id: id, key: 'alias'); - } + String name = await _getAlias(id); var controller = TextEditingController(text: name); gFFI.dialogManager.show((setState, close) { submit() async { @@ -636,7 +627,7 @@ abstract class BasePeerCard extends StatelessWidget { String name = controller.text.trim(); await bind.mainSetPeerAlias(id: id, alias: name); gFFI.abModel.setPeerAlias(id, name); - update(); + _update(); close(); isInProgress.value = false; } @@ -671,34 +662,13 @@ abstract class BasePeerCard extends StatelessWidget { }); } - void update() { - switch (cardType) { - case CardType.recent: - bind.mainLoadRecentPeers(); - break; - case CardType.fav: - bind.mainLoadFavPeers(); - break; - case CardType.lan: - bind.mainLoadLanPeers(); - break; - case CardType.ab: - gFFI.abModel.pullAb(); - break; - case CardType.grp: - gFFI.groupModel.pull(); - break; - } - } + @protected + void _update(); } class RecentPeerCard extends BasePeerCard { RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.recent, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -732,15 +702,15 @@ class RecentPeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadRecentPeers(); } class FavoritePeerCard extends BasePeerCard { FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.fav, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -776,15 +746,15 @@ class FavoritePeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadFavPeers(); } class DiscoveredPeerCard extends BasePeerCard { DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.lan, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -811,15 +781,15 @@ class DiscoveredPeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadLanPeers(); } class AddressBookPeerCard extends BasePeerCard { AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.ab, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -851,6 +821,20 @@ class AddressBookPeerCard extends BasePeerCard { return menuItems; } + @protected + @override + Future _isForceAlwaysRelay(String id) async => + gFFI.abModel.find(id)?.forceAlwaysRelay ?? false; + + @protected + @override + Future _getAlias(String id) async => + gFFI.abModel.find(id)?.alias ?? ''; + + @protected + @override + void _update() => gFFI.abModel.pullAb(); + @protected @override MenuEntryBase _removeAction( @@ -943,11 +927,7 @@ class AddressBookPeerCard extends BasePeerCard { class MyGroupPeerCard extends BasePeerCard { MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.grp, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -974,18 +954,15 @@ class MyGroupPeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => gFFI.groupModel.pull(); } -void _rdpDialog(String id, CardType card) async { - String port, username; - if (card == CardType.ab) { - port = gFFI.abModel.find(id)?.rdpPort ?? ''; - username = gFFI.abModel.find(id)?.rdpUsername ?? ''; - } else { - port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); - username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); - } - +void _rdpDialog(String id) async { + final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); + final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final portController = TextEditingController(text: port); final userController = TextEditingController(text: username); final passwordController = TextEditingController( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index ec494cf22..d4fcd16e9 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -831,7 +831,7 @@ class _TabState extends State<_Tab> with RestorationMixin { return ConstrainedBox( constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), child: Text( - translate(widget.label.value), + widget.label.value, textAlign: TextAlign.center, style: TextStyle( color: isSelected From 24660628a5c9ca3571978a249230106b7faf8305 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:31:27 -0700 Subject: [PATCH 202/734] enable i686 --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b33a6dba0..8344e562c 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: job: - # - { target: i686-pc-windows-msvc , os: windows-2019 } + - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: From 5aa4c420a4368c96fdb65eaee191a258ba8845aa Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:38:18 -0700 Subject: [PATCH 203/734] update flutter version to 3.3.10 --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 8344e562c..600dd47a2 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -9,7 +9,7 @@ on: env: LLVM_VERSION: "10.0" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.3.10" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility From 9793b35ad6c5c01d967bb8b9f36d00bdbb623e66 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:40:55 -0700 Subject: [PATCH 204/734] Update flutter-ci.yml --- .github/workflows/flutter-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 4e98f311d..9dc2bac7d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -14,7 +14,7 @@ on: env: LLVM_VERSION: "10.0" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.3.10" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" From 0838a77908c07e6443a80465077eb7f0403cc0a0 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:41:48 -0700 Subject: [PATCH 205/734] update llvm version --- .github/workflows/flutter-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 9dc2bac7d..67c485fa7 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -12,7 +12,7 @@ on: - ".github/**" env: - LLVM_VERSION: "10.0" + LLVM_VERSION: "15.0.6" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.3.10" # vcpkg version: 2022.05.10 From fb641900755f15e61952a487d0d47f996fc150a1 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:42:09 -0700 Subject: [PATCH 206/734] update llvm version --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 600dd47a2..cc6aa2eb8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - LLVM_VERSION: "10.0" + LLVM_VERSION: "15.0.6" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.3.10" TAG_NAME: "nightly" From 3e0ae64d61fdb4b6bb1f1ea5b5d7b713e1487f0c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:48:11 -0700 Subject: [PATCH 207/734] remove custom flutter from windows --- .github/workflows/flutter-ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 67c485fa7..e7ddba331 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -47,13 +47,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Replace engine with rustdesk custom flutter engine - run: | - flutter doctor -v - flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + #- name: Replace engine with rustdesk custom flutter engine + # run: | + # flutter doctor -v + # flutter precache --windows + # Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + # Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + # mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From 5ee350d58d9d6fd7a3386d0dd706ca0e641ddda8 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:53:39 -0700 Subject: [PATCH 208/734] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index cc6aa2eb8..362161fae 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -49,13 +49,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Replace engine with rustdesk custom flutter engine - run: | - flutter doctor -v - flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + #- name: Replace engine with rustdesk custom flutter engine + # run: | + # flutter doctor -v + # flutter precache --windows + # Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + # Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + # mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From 7f13f28d2921e4cc7a6499ba2ae127b82000663a Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 21:13:25 -0700 Subject: [PATCH 209/734] dont need to install rust toolchain twice --- .github/workflows/flutter-ci.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index e7ddba331..feaefc115 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -503,14 +503,6 @@ jobs: key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} cache-directories: "/opt/rust-registry" - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - name: Install local registry run: | mkdir -p /opt/rust-registry @@ -669,14 +661,6 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v2 with: prefix-key: rustdesk-lib-cache From 35a7e4f8b730b553fbad6fc80cc9aec2450e92fc Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 21:16:46 -0700 Subject: [PATCH 210/734] dont need rust toolchain twice --- .github/workflows/flutter-nightly.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 362161fae..b26b6bc38 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -649,14 +649,6 @@ jobs: key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} cache-directories: "/opt/rust-registry" - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - name: Install local registry run: | mkdir -p /opt/rust-registry @@ -815,14 +807,6 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v2 with: prefix-key: rustdesk-lib-cache From 5f0aff55009c601bdf7d352c969f04af58732a2a Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 21:21:37 -0700 Subject: [PATCH 211/734] enable i686 --- .github/workflows/flutter-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index feaefc115..efcd02170 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -28,7 +28,7 @@ jobs: fail-fast: true matrix: job: - # - { target: i686-pc-windows-msvc , os: windows-2019 } + - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: From f1236f42e1824d74f1ea8a734dfad0e34016cbc1 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 21:29:00 -0700 Subject: [PATCH 212/734] go back to 3.0.5 --- .github/workflows/flutter-nightly.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b26b6bc38..6000552c7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -9,7 +9,7 @@ on: env: LLVM_VERSION: "15.0.6" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.3.10" + FLUTTER_VERSION: "3.0.5" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility @@ -49,13 +49,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - #- name: Replace engine with rustdesk custom flutter engine - # run: | - # flutter doctor -v - # flutter precache --windows - # Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - # Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - # mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From a8dd49d85f1fc0c31856b2c4d705feab27c373c8 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 22:04:39 -0700 Subject: [PATCH 213/734] revert 3.0.5 --- .github/workflows/flutter-ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index efcd02170..34730984f 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -14,7 +14,7 @@ on: env: LLVM_VERSION: "15.0.6" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.3.10" + FLUTTER_VERSION: "3.0.5" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -47,13 +47,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - #- name: Replace engine with rustdesk custom flutter engine - # run: | - # flutter doctor -v - # flutter precache --windows - # Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - # Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - # mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From bb6501c3f5b5697827136499a7ca819502edf807 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 23 Jan 2023 18:25:52 +0800 Subject: [PATCH 214/734] fix: rename cm individual process window https://github.com/rustdesk/rustdesk/issues/2904 --- flutter/windows/runner/main.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index b7fa64dc0..f1ea6e579 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -96,10 +96,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, flutter::DartProject project(L"data"); // connection manager hide icon from taskbar - bool showOnTaskBar = true; + bool is_cm_page = false; auto cmParam = std::string("--cm"); if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, cmParam.size(), cmParam.c_str()) == 0) { - showOnTaskBar = false; + is_cm_page = true; } command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end()); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); @@ -107,9 +107,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(800, 600); - if (!window.CreateAndShow(L"RustDesk", origin, size, showOnTaskBar)) - { - return EXIT_FAILURE; + if (!window.CreateAndShow( + is_cm_page ? L"RustDesk - Connection Manager" : L"RustDesk", origin, + size, !is_cm_page)) { + return EXIT_FAILURE; } window.SetQuitOnClose(true); From 9acec720a3966f430b6ef0ff65c194a49c3609c5 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 23 Jan 2023 14:57:04 +0100 Subject: [PATCH 215/734] updated mac icon - #2722 --- flutter/pubspec.yaml | 1 + res/mac-icon.png | Bin 0 -> 90116 bytes 2 files changed, 1 insertion(+) create mode 100644 res/mac-icon.png diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a63f49ba8..a5535c8b7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -113,6 +113,7 @@ icons_launcher: enable: true macos: enable: true + image_path: "../res/mac-icon.png" linux: enable: true diff --git a/res/mac-icon.png b/res/mac-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a9813152ef248d07076f956a3642575c396c0811 GIT binary patch literal 90116 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 z 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 av)BBzn526< Date: Mon, 23 Jan 2023 22:07:50 +0800 Subject: [PATCH 216/734] opt: update remote alias/id on taskbar in remote window https://github.com/rustdesk/rustdesk/discussions/2815#discussioncomment-4752398 --- flutter/lib/common.dart | 34 +++++++++++--- .../desktop/pages/file_manager_tab_page.dart | 4 ++ .../desktop/pages/port_forward_tab_page.dart | 4 ++ .../lib/desktop/pages/remote_tab_page.dart | 9 +++- flutter/lib/desktop/pages/server_page.dart | 7 ++- .../lib/desktop/widgets/refresh_wrapper.dart | 2 +- .../lib/desktop/widgets/tabbar_widget.dart | 14 +++--- flutter/lib/main.dart | 47 +++++++++---------- flutter/lib/utils/multi_window_manager.dart | 9 ++-- 9 files changed, 85 insertions(+), 45 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 1aead0fd4..f4e0c2d75 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1263,23 +1263,23 @@ StreamSubscription? listenUniLinks() { bool checkArguments() { // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args - final connectIndex = bootArgs.indexOf("--connect"); + final connectIndex = kBootArgs.indexOf("--connect"); if (connectIndex == -1) { return false; } String? id = - bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1]; - final switchUuidIndex = bootArgs.indexOf("--switch_uuid"); - String? switchUuid = bootArgs.length < switchUuidIndex + 1 + kBootArgs.length < connectIndex + 1 ? null : kBootArgs[connectIndex + 1]; + final switchUuidIndex = kBootArgs.indexOf("--switch_uuid"); + String? switchUuid = kBootArgs.length < switchUuidIndex + 1 ? null - : bootArgs[switchUuidIndex + 1]; + : kBootArgs[switchUuidIndex + 1]; if (id != null) { if (id.startsWith(kUniLinksPrefix)) { return parseRustdeskUri(id); } else { // remove "--connect xxx" in the `bootArgs` array - bootArgs.removeAt(connectIndex); - bootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); // fallback to peer id Future.delayed(Duration.zero, () { rustDeskWinManager.newRemoteDesktop(id, switch_uuid: switchUuid); @@ -1617,3 +1617,23 @@ Widget dialogButton(String text, int version_cmp(String v1, String v2) { return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); } + +String getWindowName({WindowType? overrideType}) { + switch (overrideType ?? kWindowType) { + case WindowType.Main: + return "RustDesk"; + case WindowType.FileTransfer: + return "File Transfer - RustDesk"; + case WindowType.PortForward: + return "Port Forward - RustDesk"; + case WindowType.RemoteDesktop: + return "Remote Desktop - RustDesk"; + default: + break; + } + return "RustDesk"; +} + +String getWindowNameWithId(String id, {WindowType? overrideType}) { + return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; +} diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 7e07eaa9a..b2566e267 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -31,6 +31,10 @@ class _FileManagerTabPageState extends State { _FileManagerTabPageState(Map params) { Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); + tabController.onSelected = (_, id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: params['id'], label: params['id'], diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index d4c0a86f8..ca354f297 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -31,6 +31,10 @@ class _PortForwardTabPageState extends State { isRDP = params['isRDP']; tabController = Get.put(DesktopTabController(tabType: DesktopTabType.portForward)); + tabController.onSelected = (_, id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: params['id'], label: params['id'], diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index a3532d49a..83928c3fe 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -39,8 +39,7 @@ class ConnectionTabPage extends StatefulWidget { class _ConnectionTabPageState extends State { final tabController = Get.put(DesktopTabController( - tabType: DesktopTabType.remoteScreen, - onSelected: (_, id) => bind.setCurSessionId(id: id))); + tabType: DesktopTabType.remoteScreen)); static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined; @@ -54,6 +53,11 @@ class _ConnectionTabPageState extends State { final peerId = params['id']; if (peerId != null) { ConnectionTypeState.init(peerId); + tabController.onSelected = (_, id) { + bind.setCurSessionId(id: id); + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: peerId, label: peerId, @@ -76,6 +80,7 @@ class _ConnectionTabPageState extends State { super.initState(); tabController.onRemoved = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 8c8679e96..521413647 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -30,7 +30,12 @@ class _DesktopServerPageState extends State void initState() { gFFI.ffiModel.updateEventListener(""); windowManager.addListener(this); - tabController.onRemoved = (_, id) => onRemoveId(id); + tabController.onRemoved = (_, id) { + onRemoveId(id); + }; + tabController.onSelected = (_, id) { + windowManager.setTitle(getWindowNameWithId(id)); + }; super.initState(); } diff --git a/flutter/lib/desktop/widgets/refresh_wrapper.dart b/flutter/lib/desktop/widgets/refresh_wrapper.dart index 4f2795d71..60e816044 100644 --- a/flutter/lib/desktop/widgets/refresh_wrapper.dart +++ b/flutter/lib/desktop/widgets/refresh_wrapper.dart @@ -26,7 +26,7 @@ class RefreshWrapperState extends State { } rebuild() { - debugPrint("=====Global State Rebuild (win-${windowId ?? 'main'})====="); + debugPrint("=====Global State Rebuild (win-${kWindowId ?? 'main'})====="); if (Get.context != null) { (context as Element).visitChildren(_rebuildElement); } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d4fcd16e9..ddc0e7729 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -486,7 +486,7 @@ class WindowActionPanelState extends State } }); } else { - final wc = WindowController.fromWindowId(windowId!); + final wc = WindowController.fromWindowId(kWindowId!); wc.isMaximized().then((maximized) { debugPrint("isMaximized $maximized"); if (widget.isMaximized.value != maximized) { @@ -534,10 +534,10 @@ class WindowActionPanelState extends State await windowManager.hide(); } else { // it's safe to hide the subwindow - await WindowController.fromWindowId(windowId!).hide(); + await WindowController.fromWindowId(kWindowId!).hide(); await Future.wait([ rustDeskWinManager - .call(WindowType.Main, kWindowEventHide, {"id": windowId!}), + .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), widget.onClose?.call() ?? Future.microtask(() => null) ]); } @@ -563,7 +563,7 @@ class WindowActionPanelState extends State if (widget.isMainWindow) { windowManager.minimize(); } else { - WindowController.fromWindowId(windowId!).minimize(); + WindowController.fromWindowId(kWindowId!).minimize(); } }, isClose: false, @@ -593,7 +593,7 @@ class WindowActionPanelState extends State if (widget.isMainWindow) { await windowManager.close(); } else { - await WindowController.fromWindowId(windowId!) + await WindowController.fromWindowId(kWindowId!) .close(); } }); @@ -622,7 +622,7 @@ void startDragging(bool isMainWindow) { if (isMainWindow) { windowManager.startDragging(); } else { - WindowController.fromWindowId(windowId!).startDragging(); + WindowController.fromWindowId(kWindowId!).startDragging(); } } @@ -638,7 +638,7 @@ Future toggleMaximize(bool isMainWindow) async { return true; } } else { - final wc = WindowController.fromWindowId(windowId!); + final wc = WindowController.fromWindowId(kWindowId!); if (await wc.isMaximized()) { wc.unmaximize(); return false; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6593f1804..1ec963f22 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -26,13 +26,15 @@ import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; import 'models/platform_model.dart'; -int? windowId; -late List bootArgs; +/// Basic window and launch properties. +int? kWindowId; +WindowType? kWindowType; +late List kBootArgs; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); - bootArgs = List.from(args); + kBootArgs = List.from(args); if (!isDesktop) { runMobileApp(); @@ -40,10 +42,10 @@ Future main(List args) async { } // main window if (args.isNotEmpty && args.first == 'multi_window') { - windowId = int.parse(args[1]); - stateGlobal.setWindowId(windowId!); + kWindowId = int.parse(args[1]); + stateGlobal.setWindowId(kWindowId!); if (!Platform.isMacOS) { - WindowController.fromWindowId(windowId!).showTitleBar(false); + WindowController.fromWindowId(kWindowId!).showTitleBar(false); } final argument = args[2].isEmpty ? {} @@ -51,35 +53,32 @@ Future main(List args) async { int type = argument['type'] ?? -1; // to-do: No need to parse window id ? // Because stateGlobal.windowId is a global value. - argument['windowId'] = windowId; - WindowType wType = type.windowType; - switch (wType) { + argument['windowId'] = kWindowId; + kWindowType = type.windowType; + final windowName = getWindowName(); + switch (kWindowType) { case WindowType.RemoteDesktop: desktopType = DesktopType.remote; runMultiWindow( argument, kAppTypeDesktopRemote, - 'RustDesk - Remote Desktop', + windowName, ); - WindowController.fromWindowId(windowId!) - .setTitle('RustDesk - Remote Desktop'); break; case WindowType.FileTransfer: desktopType = DesktopType.fileTransfer; runMultiWindow( argument, kAppTypeDesktopFileTransfer, - 'RustDesk - File Transfer', + windowName, ); - WindowController.fromWindowId(windowId!) - .setTitle('RustDesk - File Transfer'); break; case WindowType.PortForward: desktopType = DesktopType.portForward; runMultiWindow( argument, kAppTypeDesktopPortForward, - 'RustDesk - Port Forward', + windowName, ); break; default: @@ -139,7 +138,7 @@ void runMainApp(bool startService) async { windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.setOpacity(1); }); - windowManager.setTitle("RustDesk"); + windowManager.setTitle(getWindowName()); } void runMobileApp() async { @@ -155,7 +154,7 @@ void runMultiWindow( ) async { await initEnv(appType); // set prevent close to true, we handle close event manually - WindowController.fromWindowId(windowId!).setPreventClose(true); + WindowController.fromWindowId(kWindowId!).setPreventClose(true); late Widget widget; switch (appType) { case kAppTypeDesktopRemote: @@ -184,26 +183,26 @@ void runMultiWindow( ); // we do not hide titlebar on win7 because of the frame overflow. if (kUseCompatibleUiMode) { - WindowController.fromWindowId(windowId!).showTitleBar(true); + WindowController.fromWindowId(kWindowId!).showTitleBar(true); } switch (appType) { case kAppTypeDesktopRemote: await restoreWindowPosition(WindowType.RemoteDesktop, - windowId: windowId!); + windowId: kWindowId!); break; case kAppTypeDesktopFileTransfer: - await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId!); + await restoreWindowPosition(WindowType.FileTransfer, + windowId: kWindowId!); break; case kAppTypeDesktopPortForward: - await restoreWindowPosition(WindowType.PortForward, windowId: windowId!); + await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; default: // no such appType exit(0); } // show window from hidden status - WindowController.fromWindowId(windowId!).show(); - WindowController.fromWindowId(windowId!).setTitle(title); + WindowController.fromWindowId(kWindowId!).show(); } void runConnectionManagerScreen(bool hide) async { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 5087538c5..ee19ac485 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -62,7 +62,8 @@ class RustDeskMultiWindowManager { remoteDesktopController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - remote desktop") + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.RemoteDesktop)) ..show(); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; @@ -88,7 +89,8 @@ class RustDeskMultiWindowManager { fileTransferController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - file transfer") + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.FileTransfer)) ..show(); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; @@ -114,7 +116,8 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - port forward") + ..setTitle( + getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) ..show(); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; From d4851ebb4009e89f3de5fe81edd27409f158d26d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 24 Jan 2023 01:24:53 +0800 Subject: [PATCH 217/734] revert 8fb3c452bea8dfb9c1de14db8b06f158d31147dd --- flutter/pubspec.lock | 66 +++++++++++++++++++++++++++++++---- libs/hbb_common/src/config.rs | 10 ++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ef57f375c..15a1a23ac 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -78,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.1" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" build_config: dependency: transitive description: @@ -169,6 +176,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -190,6 +204,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + colorize: + dependency: transitive + description: + name: colorize + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" contextmenu: dependency: "direct main" description: @@ -339,6 +360,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + ffigen: + dependency: "direct dev" + description: + name: ffigen + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.4" file: dependency: transitive description: @@ -429,12 +457,10 @@ packages: flutter_rust_bridge: dependency: "direct main" description: - path: frb_dart - ref: master - resolved-ref: e5adce55eea0b74d3680e66a2c5252edf17b07e1 - url: "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" - source: git - version: "1.32.0" + name: flutter_rust_bridge + url: "https://pub.dartlang.org" + source: hosted + version: "1.61.1" flutter_svg: dependency: "direct main" description: @@ -846,6 +872,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" + puppeteer: + dependency: transitive + description: + name: puppeteer + url: "https://pub.dartlang.org" + source: hosted + version: "2.12.0" qr_code_scanner: dependency: "direct main" description: @@ -853,6 +886,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" rxdart: dependency: transitive description: @@ -890,6 +930,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" shelf_web_socket: dependency: transitive description: @@ -1256,6 +1303,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.1" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" zxing2: dependency: "direct main" description: diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index abec5b231..1e3bde9eb 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -49,10 +49,7 @@ lazy_static::lazy_static! { static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::load())); pub static ref ONLINE: Arc>> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Arc::new(RwLock::new(match option_env!("RENDEZVOUS_SERVER") { - Some(key) => key, - _ => "", - }.to_owned())); + pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); @@ -86,10 +83,7 @@ const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { - Some(key) => key, - None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", -}; +pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; From efa7b52f49dcd0c39d6b7971785ede8558a0495c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 24 Jan 2023 01:32:56 +0800 Subject: [PATCH 218/734] fix nightly build RS_PUB_KEY issue --- libs/hbb_common/src/config.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1e3bde9eb..20334ed12 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -49,7 +49,10 @@ lazy_static::lazy_static! { static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::load())); pub static ref ONLINE: Arc>> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); + pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Arc::new(RwLock::new(match option_env!("RENDEZVOUS_SERVER") { + Some(key) if !key.is_empty() => key, + _ => "", + }.to_owned())); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); @@ -83,7 +86,10 @@ const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; +pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { + Some(key) if !key.is_empty() => key, + _ => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", +}; pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; From e66ecae5f4a2b9b97c6aff228def9652488145c6 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 23 Jan 2023 18:57:16 +0100 Subject: [PATCH 219/734] generated new mac icons --- .../AppIcon.appiconset/app_icon_1024.png | Bin 13838 -> 100419 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 1517 -> 5792 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 349 -> 569 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 3103 -> 14429 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 562 -> 1256 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 6186 -> 37270 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 901 -> 2618 bytes 7 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 f51492c53e43124bfb81705e26f7e59004cf9c88..4b6ea50696ab62a80cf4eaa9ce6d344f8c478917 100644 GIT binary patch literal 100419 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%0DUB){U4Ess|GV&(7(tHoy(1AJxTUO5(rOw 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 BYk&X% diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 97555e4a23b225b6366cfc624c50b4ba2da6117b..c561ee3b4ca9f9e4eb4dd731c2091a723e886fdf 100644 GIT binary patch literal 5792 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 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 c695fd3a37f48b7ec7dc4f7a5111800044b4e3b3..3388501fc5df22fd183e482b7bfe514356a2fe15 100644 GIT binary patch literal 14429 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 z

    72(C71Sw3~UF7&R`t>H@17tOC{8+r@!OUAEuALGN__E3SA zMf`s|vi?$s29ORL*%;M;3SlN=fsGc3>M%&B>xn0br9pYLGrfbCZ6ZfP*TSFz9J;ma?M>eJSuQoPvIvto+*)`PYdJgxyzi$c9^As;uQo zU0jG3gTwX_ds;k@n%PWt9#LAocd!$!5T+ncAQ8}K63%add(@cAPWwlcESFnbo_ju| z?~ltzcgr{KwY}YRYur)evp#~ESwH?B@^(CHCI}tl2RQ$THxZDSHf~*`qhNwKtmTmg zSg~sSuWgGvG*1gA&*mS0MMbR2PT&0;Z*|Clz>(bJvTw7aqjk3}7U6Ip8e;Cq?-AAL zmM3ola2!Z{-2fh=_^6TJXu^ihNasB$^6P}%ICxtK zlCL*bHS-y1Vx=Cu9|dI}S*A_W9>=GRy5K_$3HY>yahg!8LHB1c46UF5- zhttF;5EzKQ_wELVs2q3ii54^nFsF9F=rk!xoYcuN0M|q(h{_hbE6v!6r9-})} zPSc$lIx)+#&TxC&#VOzAhDb0Mp3@e;;PUVvr<#1;BN((m8j2H?|Dir+9=cS(>36g& zddBFd5)t+=UYPf=#%=21P{x?Yv-jfVx}s`yc=Pe#L7J74))97d!^+ySLw268>XCTu ziXt5dm_$#Ej&)d+&VKBD@X>Pb$aM7Z&P}2Jt8B39Iuev{@J@jKPdqj#^qG|VN)*}R z

    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 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 daacb85ae376f30551702e9ba0ef7bf56fb981a7..76d846c4a66adbeedbed97062f890cbc5832570e 100644 GIT binary patch delta 1213 zcmV;u1Va0|6vhdV8Gi!+005o0f$RVP0eVnOR7C&)003m+0A=I}K$QSgzY0N^0At_) zRKEaY;R+To0B`&Oas2^f;{a5@093yKRl){ClmKe+0BZ06Wa9;F?*L@s0At<(P_zLt zZwo(+4l{HKENKriZV)bJ3Kuj0Y3>7T^8j4Y0AbnzVAud()PDdya7q2 z0!ya?H+}?4q!BrQ5;}GVBvlJLga8&X0CE5S|Ns5|{|9yd?DhYd#{VLO{{(aYSLp{|$Nn_xk_n^8eQ8|G3!yn8yE=#Q%uB|9Q3lZm$1g zr~g-?{~Lh+5_|vA<^Rp&|HIz@z})|-&;NnB|9!XrQJ()un*TtQ|K9EY+Uozu;QzJO z|E$sfkiq|rzyDjL|4p3#DvAH__W#-H|IXw8yV?J;)PMh<%KvAo|2L2SFN^;<7fqu8 z002XDQchC<%HA`+GsC&Y2Kx2aySA)E?dsgu$-sqVFBTIJ3k2oe#JaVup_-J5dva+@ zH7zVC9@5dSuA`BEd3J7XXGuacDJVOE^`ig)0=P*;K~z}7?UiR!+dvRNMI1UV^ll&t zEr9^(y??Ecd+)trY)lCR(v$xlTX(+Eog|+%lg#7;Z^li#`*gRKR>C#^A0K8mRv%j| z{r&xB^GyY2bMuhJ@@REqW=FqzaApL$VEomfcKr@6=QHt6tN!gUbj|P^b^U#+o@>}v z?SBFcvCQ`29vI{%LQ>a;7N36cwxOiy~P;N^J9u z+QtAS=u?LLA|VTq5eW%pFDEI%fFg24&L6u707TAX#BiZV>!AcaKq$8JPxAPL?tHx6 zhkr&Ff+S8>_RO8etBSk10vG-Rd_a-KhCEkrjuNYMJ-HkzJb~LG=Q)b3AQD4eC~yP~ zbfQC0QA3d&M{tPF<5CM;NRZ?R&g85wpn*pY9dHCEa@Kb(!GRn)<_J&^Lt285sJ;JG zKsK7t--4GTD5EU>CI&fzswkuXH-@wYlYh)C$N+(fWElZF4}RkbsG6aY7e%f(0*I5F zktU6fmw+e8;?YrOraZo#f9eSOR7;KKl=T^0e=KnYP$wg_L8fG|J<=|LFMzPv##@sB z0%)~mO-^)ktC=-BN0z4<}m bn!n8-#h{tR4FTb;00000NkvXXu0mjfuMswj_X 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 From 1d00e321288d05f672ec480323960c5fc5480f12 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 24 Jan 2023 03:04:45 -0700 Subject: [PATCH 223/734] i686 doesnt work --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 6000552c7..1f4fcf2ef 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: job: - - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: From 4ee0891030b4f73b94df9bac76e8de0aad53b8b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=83lina-Ioana=20Popa?= Date: Tue, 24 Jan 2023 16:59:31 +0100 Subject: [PATCH 224/734] add romanian language --- src/lang.rs | 3 + src/lang/ro.rs | 401 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 src/lang/ro.rs diff --git a/src/lang.rs b/src/lang.rs index 6254e988a..9e6ea1b5b 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -16,6 +16,7 @@ mod ja; mod ko; mod pl; mod ptbr; +mod ro; mod ru; mod sk; mod tr; @@ -57,6 +58,7 @@ lazy_static::lazy_static! { ("ca", "Català"), ("gr", "Ελληνικά"), ("sv", "Svenska"), + ("ro", "Română"), ]); } @@ -111,6 +113,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ca" => ca::T.deref(), "gr" => gr::T.deref(), "sv" => sv::T.deref(), + "ro" => ro::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/ro.rs b/src/lang/ro.rs new file mode 100644 index 000000000..c5a9b529c --- /dev/null +++ b/src/lang/ro.rs @@ -0,0 +1,401 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stare"), + ("Your Desktop", "Desktopul tău"), + ("desk_tip", "Desktopul tău poate fi accesat folosind ID-ul și parola de mai jos."), + ("Password", "Parola"), + ("Ready", "Pregătit"), + ("Established", "Stabilit"), + ("connecting_status", "În curs de conectare la rețeaua RustDesk..."), + ("Enable Service", "Activează serviciu"), + ("Start Service", "Pornește serviciu"), + ("Service is running", "Serviciul este în curs de executare..."), + ("Service is not running", "Serviciul nu funcționează"), + ("not_ready_status", "Nepregătit. Verifică conexiunea la rețea."), + ("Control Remote Desktop", "Controlează desktop-ul la distanță"), + ("Transfer File", "Transferă fișier"), + ("Connect", "Conectează-te"), + ("Recent Sessions", "Sesiuni recente"), + ("Address Book", "Agendă"), + ("Confirmation", "Confirmare"), + ("TCP Tunneling", "Tunel TCP"), + ("Remove", "Elimină"), + ("Refresh random password", "Actualizează parolă aleatorie"), + ("Set your own password", "Setează propria parolă"), + ("Enable Keyboard/Mouse", "Activează control tastatură/mouse"), + ("Enable Clipboard", "Activează clipboard"), + ("Enable File Transfer", "Activează transfer fișiere"), + ("Enable TCP Tunneling", "Activează tunel TCP"), + ("IP Whitelisting", "Listă de IP-uri autorizate"), + ("ID/Relay Server", "Server de ID/retransmisie"), + ("Import Server Config", "Importă configurație server"), + ("Export Server Config", "Exportă configurație server"), + ("Import server configuration successfully", "Configurație server importată cu succes"), + ("Export server configuration successfully", "Configurație server exportată cu succes"), + ("Invalid server configuration", "Configurație server nevalidă"), + ("Clipboard is empty", "Clipboard gol"), + ("Stop service", "Oprește serviciu"), + ("Change ID", "Schimbă ID"), + ("Website", "Site web"), + ("About", "Despre"), + ("Mute", "Fără sunet"), + ("Audio Input", "Intrare audio"), + ("Enhancements", "Îmbunătățiri"), + ("Hardware Codec", "Codec hardware"), + ("Adaptive Bitrate", "Rată de biți adaptabilă"), + ("ID Server", "Server de ID"), + ("Relay Server", "Server de retransmisie"), + ("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"), + ("Too frequent", "Modificat prea frecvent"), + ("Cancel", "Anulează"), + ("Skip", "Omite"), + ("Close", "Închide"), + ("Retry", "Reîncearcă"), + ("OK", "OK"), + ("Password Required", "Parolă necesară"), + ("Please enter your password", "Introdu parola"), + ("Remember password", "Memorează parola"), + ("Wrong Password", "Parolă incorectă"), + ("Do you want to enter again?", "Vrei să intri din nou?"), + ("Connection Error", "Eroare de conexiune"), + ("Error", "Eroare"), + ("Reset by the peer", "Conexiunea a fost închisă de dispozitivul pereche"), + ("Connecting...", "Conectare..."), + ("Connection in progress. Please wait.", "Conectare în curs. Te rugăm așteaptă."), + ("Please try 1 minute later", "Reîncearcă într-un minut"), + ("Login Error", "Eroare de autentificare"), + ("Successful", "Succes"), + ("Connected, waiting for image...", "Conectat, se așteaptă transmiterea imaginii..."), + ("Name", "Denumire"), + ("Type", "Tip"), + ("Modified", "Modificat"), + ("Size", "Dimensiune"), + ("Show Hidden Files", "Afișează fișiere ascunse"), + ("Receive", "Acceptă"), + ("Send", "Trimite"), + ("Refresh File", "Actualizează fișier"), + ("Local", "Local"), + ("Remote", "La distanță"), + ("Remote Computer", "Computer la distanță"), + ("Local Computer", "Computer local"), + ("Confirm Delete", "Confirmă ștergerea"), + ("Delete", "Șterge"), + ("Properties", "Caracteristici"), + ("Multi Select", "Alegere multiplă"), + ("Select All", "Selectează tot"), + ("Unselect All", "Deselectează tot"), + ("Empty Directory", "Director gol"), + ("Not an empty directory", "Directorul nu este gol"), + ("Are you sure you want to delete this file?", "Sigur vrei să ștergi acest fișier?"), + ("Are you sure you want to delete this empty directory?", "Sigur vrei să ștergi acest director gol?"), + ("Are you sure you want to delete the file of this directory?", "Sigur vrei să ștergi fișierul din acest director?"), + ("Do this for all conflicts", "Aplică pentru toate conflictele"), + ("This is irreversible!", "Această acțiune este ireversibilă!"), + ("Deleting", "În curs de ștergere..."), + ("files", "fișier"), + ("Waiting", "În așteptare..."), + ("Finished", "Finalizat"), + ("Speed", "Viteză"), + ("Custom Image Quality", "Setează calitatea imaginii"), + ("Privacy mode", "Mod privat"), + ("Block user input", "Blochează utilizator"), + ("Unblock user input", "Deblochează utilizator"), + ("Adjust Window", "Ajustează fereastra"), + ("Original", "Dimensiune originală"), + ("Shrink", "Micșorează"), + ("Stretch", "Extinde"), + ("Scrollbar", "Bară de derulare"), + ("ScrollAuto", "Derulare automată"), + ("Good image quality", "Calitate bună a imaginii"), + ("Balanced", "Calitate normală a imaginii"), + ("Optimize reaction time", "Optimizează timpul de reacție"), + ("Custom", "Personalizare"), + ("Show remote cursor", "Afișează cursor la distanță"), + ("Show quality monitor", "Afișează indicator de calitate"), + ("Disable clipboard", "Dezactivează clipboard"), + ("Lock after session end", "Blochează după deconectare"), + ("Insert", "Introdu"), + ("Insert Lock", "Blochează computer"), + ("Refresh", "Reîmprospătează"), + ("ID does not exist", "ID neexistent"), + ("Failed to connect to rendezvous server", "Conectare la server rendezvous eșuată"), + ("Please try later", "Încearcă mai târziu"), + ("Remote desktop is offline", "Desktopul la distanță este offline"), + ("Key mismatch", "Nepotrivire chei"), + ("Timeout", "Conexiune expirată"), + ("Failed to connect to relay server", "Conectare la server de retransmisie eșuată"), + ("Failed to connect via rendezvous server", "Conectare prin intermediul serverului rendezvous eșuată"), + ("Failed to connect via relay server", "Conectare prin intermediul serverului de retransmisie eșuată"), + ("Failed to make direct connection to remote desktop", "Imposibil de stabilit o conexiune directă cu desktopul la distanță"), + ("Set Password", "Setează parola"), + ("OS Password", "Parolă OS"), + ("install_tip", "Din cauza restricțiilor UAC, e posibil ca RustDesk să nu funcționeze corespunzător. Pentru a evita acest lucru, fă clic pe butonul de mai jos pentru a instala RustDesk."), + ("Click to upgrade", "Fă clic pentru a face upgrade"), + ("Click to download", "Fă clic pentru a descărca"), + ("Click to update", "Fă clic pentru a actualiza"), + ("Configure", "Configurează"), + ("config_acc", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Accesibilitate."), + ("config_screen", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Înregistrare ecran."), + ("Installing ...", "Instalare în curs..."), + ("Install", "Instalează"), + ("Installation", "Instalare"), + ("Installation Path", "Cale de instalare"), + ("Create start menu shortcuts", "Creează comenzi rapide în meniul Start"), + ("Create desktop icon", "Creează pictogramă pe desktop"), + ("agreement_tip", "Începerea procesului de instalare înseamnă acceptarea acordului de licență."), + ("Accept and Install", "Acceptă și instalează"), + ("End-user license agreement", "Acord de licență pentru utilizatorul final"), + ("Generating ...", "Se generează..."), + ("Your installation is lower version.", "Versiunea instalată este una inferioară."), + ("not_close_tcp_tip", "Nu închide această fereastră în timp ce folosești tunelul"), + ("Listening ...", "În așteptarea conexiunii tunel..."), + ("Remote Host", "Gazdă la distanță"), + ("Remote Port", "Port la distanță"), + ("Action", "Acțiune"), + ("Add", "Adaugă"), + ("Local Port", "Port local"), + ("Local Address", "Adresă locală"), + ("Change Local Port", "Schimbă port local"), + ("setup_server_tip", "Pentru o conexiune mai rapidă, îți poți configura propriul server."), + ("Too short, at least 6 characters.", "Prea scurt; trebuie cel puțin 6 caractere."), + ("The confirmation is not identical.", "Cele două intrări nu corespund."), + ("Permissions", "Permisiuni"), + ("Accept", "Acceptă"), + ("Dismiss", "Respinge"), + ("Disconnect", "Deconectează-te"), + ("Allow using keyboard and mouse", "Permite utilizarea tastaturii și mouse-ului"), + ("Allow using clipboard", "Permite utilizarea clipboardului"), + ("Allow hearing sound", "Permite auzirea sunetului"), + ("Allow file copy and paste", "Permite copierea/lipirea fișierelor"), + ("Connected", "Conectat"), + ("Direct and encrypted connection", "Conexiune directă criptată"), + ("Relayed and encrypted connection", "Conexiune retransmisă criptată"), + ("Direct and unencrypted connection", "Conexiune directă necriptată"), + ("Relayed and unencrypted connection", "Conexiune retransmisă necriptată"), + ("Enter Remote ID", "Introdu ID-ul dispozitivului la distanță"), + ("Enter your password", "Introdu parola"), + ("Logging in...", "Se conectează..."), + ("Enable RDP session sharing", "Activează partajarea sesiunii RDP"), + ("Auto Login", "Conectare automată (valid doar dacă funcția de Blocare după deconectare este activă)"), + ("Enable Direct IP Access", "Activează accesul direct cu IP"), + ("Rename", "Redenumește"), + ("Space", "Spațiu"), + ("Create Desktop Shortcut", "Creează comandă rapidă de desktop"), + ("Change Path", "Schimbă calea"), + ("Create Folder", "Creează folder"), + ("Please enter the folder name", "Introdu numele folderului"), + ("Fix it", "Repară"), + ("Warning", "Avertisment"), + ("Login screen using Wayland is not supported", "Ecranele de conectare care folosesc Wayland nu sunt acceptate"), + ("Reboot required", "Repornire necesară"), + ("Unsupported display server ", "Tipul de server de afișaj nu este acceptat"), + ("x11 expected", "E necesar X11"), + ("Port", "Port"), + ("Settings", "Setări"), + ("Username", " Nume de utilizator"), + ("Invalid port", "Port nevalid"), + ("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"), + ("Always connect via relay", "Se conectează mereu prin retransmisie"), + ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), + ("Login", "Conectare"), + ("Logout", "Deconectare"), + ("Tags", "Etichetare"), + ("Search ID", "Caută după ID"), + ("Current Wayland display server is not supported", "Serverul de afișaj Wayland nu este acceptat"), + ("whitelist_sep", "Poți folosi ca separator virgula, punctul și virgula, spațiul sau linia nouă"), + ("Add ID", "Adaugă ID"), + ("Add Tag", "Adaugă etichetă"), + ("Unselect all tags", "Deselectează toate etichetele"), + ("Network error", "Eroare de rețea"), + ("Username missed", "Lipsește numele de utilizator"), + ("Password missed", "Lipsește parola"), + ("Wrong credentials", "Nume sau parolă greșită"), + ("Edit Tag", "Modifică etichetă"), + ("Unremember Password", "Uită parola"), + ("Favorites", "Favorite"), + ("Add to Favorites", "Adaugă la Favorite"), + ("Remove from Favorites", "Șterge din Favorite"), + ("Empty", "Gol"), + ("Invalid folder name", "Denumire folder nevalidă"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Hostname", "Nume gazdă"), + ("Discovered", "Descoperite"), + ("install_daemon_tip", "Pentru executare la pornirea sistemului, instalează serviciul de sistem."), + ("Remote ID", "ID dispozitiv la distanță"), + ("Paste", "Lipește"), + ("Paste here?", "Lipește aici?"), + ("Are you sure to close the connection?", "Sigur vrei să închizi conexiunea?"), + ("Download new version", "Descarcă noua versiune"), + ("Touch mode", "Mod tactil"), + ("Mouse mode", "Mod mouse"), + ("One-Finger Tap", "Apasă cu un deget"), + ("Left Mouse", "Clic stânga"), + ("One-Long Tap", "Apasă lung"), + ("Two-Finger Tap", "Apasă cu două degete"), + ("Right Mouse", "Clic dreapta"), + ("One-Finger Move", "Mișcă cu un deget"), + ("Double Tap & Move", "Apasă dublu și mișcă"), + ("Mouse Drag", "Tragere mouse"), + ("Three-Finger vertically", "Trei degete vertical"), + ("Mouse Wheel", "Rotiță mouse"), + ("Two-Finger Move", "Mișcă cu două degete"), + ("Canvas Move", "Mută ecran"), + ("Pinch to Zoom", "Apropie degetele pentru zoom"), + ("Canvas Zoom", "Zoom ecran"), + ("Reset canvas", "Reinițializează ecranul"), + ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"), + ("Note", "Reține"), + ("Connection", "Conexiune"), + ("Share Screen", "Partajează ecran"), + ("CLOSE", "ÎNCHIDE"), + ("OPEN", "DESCHIDE"), + ("Chat", "Discută"), + ("Total", "Total"), + ("items", "elemente"), + ("Selected", "Selectat"), + ("Screen Capture", "Captură ecran"), + ("Input Control", "Control intrări"), + ("Audio Capture", "Captură audio"), + ("File Connection", "Conexiune fișier"), + ("Screen Connection", "Conexiune ecran"), + ("Do you accept?", "Accepți?"), + ("Open System Setting", "Deschide setări sistem"), + ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), + ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilize serviciul Accesibilitate."), + ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, caută și deschide [Aplicații instalate] și activează serviciul [RustDesk Input]."), + ("android_new_connection_tip", "Ai primit o nouă solicitare de control pentru dispozitivul actual."), + ("android_service_will_start_tip", "Activarea setării Captură ecran va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."), + ("android_stop_service_tip", "Închiderea serviciului va închide automat toate conexiunile stabilite."), + ("android_version_audio_tip", "Versiunea actuală de Android nu suportă captura audio. Fă upgrade la Android 10 sau la o versiune superioară."), + ("android_start_service_tip", "Apasă [Pornește serviciu] sau DESCHIDE [Captură ecran] pentru a porni serviciul de partajare a ecranului."), + ("Account", "Cont"), + ("Overwrite", "Suprascrie"), + ("This file exists, skip or overwrite this file?", "Fișier deja existent. Omite sau suprascrie?"), + ("Quit", "Ieși"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Ajutor"), + ("Failed", "Nereușit"), + ("Succeeded", "Reușit"), + ("Someone turns on privacy mode, exit", "Cineva activează modul privat, ieși din"), + ("Unsupported", "Neacceptat"), + ("Peer denied", "Dispozitiv pereche refuzat"), + ("Please install plugins", "Instalează pluginuri"), + ("Peer exit", "Ieșire dispozitiv pereche"), + ("Failed to turn off", "Dezactivare nereușită"), + ("Turned off", "Închis"), + ("In privacy mode", "În modul privat"), + ("Out privacy mode", "Ieșit din modul privat"), + ("Language", "Limbă"), + ("Keep RustDesk background service", "Rulează serviciul RustDesk în fundal"), + ("Ignore Battery Optimizations", "Ignoră optimizările de baterie"), + ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."), + ("Connection not allowed", "Conexiune neautoriztă"), + ("Legacy mode", "Mod legacy"), + ("Map mode", "Mod hartă"), + ("Translate mode", "Mod traducere"), + ("Use permanent password", "Folosește parola permanentă"), + ("Use both passwords", "Folosește parola unică și cea permanentă"), + ("Set permanent password", "Setează parola permanentă"), + ("Enable Remote Restart", "Activează repornirea la distanță"), + ("Allow remote restart", "Permite repornirea la distanță"), + ("Restart Remote Device", "Repornește dispozivul la distanță"), + ("Are you sure you want to restart", "Sigur vrei să repornești dispozitivul?"), + ("Restarting Remote Device", "Se repornește dispozitivul la distanță"), + ("remote_restarting_tip", "Dispozitivul este în curs de repornire. Închide acest mesaj și reconectează-te cu parola permanentă după un timp."), + ("Copied", "Copiat"), + ("Exit Fullscreen", "Ieși din modul ecran complet"), + ("Fullscreen", "Ecran complet"), + ("Mobile Actions", "Acțiuni mobile"), + ("Select Monitor", "Selectează monitor"), + ("Control Actions", "Acțiuni de control"), + ("Display Settings", "Setări afișaj"), + ("Ratio", "Raport"), + ("Image Quality", "Calitate imagine"), + ("Scroll Style", "Stil de derulare"), + ("Show Menubar", "Arată bara de meniu"), + ("Hide Menubar", "Ascunde bara de meniu"), + ("Direct Connection", "Conexiune directă"), + ("Relay Connection", "Conexiune prin retransmisie"), + ("Secure Connection", "Conexiune securizată"), + ("Insecure Connection", "Conexiune nesecurizată"), + ("Scale original", "Scală originală"), + ("Scale adaptive", "Scală adaptivă"), + ("General", "General"), + ("Security", "Securitate"), + ("Account", "Cont"), + ("Theme", "Temă"), + ("Dark Theme", "Temă întunecată"), + ("Dark", "Întunecat"), + ("Light", "Luminos"), + ("Follow System", "Urmărește sistem"), + ("Enable hardware codec", "Activează codec hardware"), + ("Unlock Security Settings", "Deblochează setări de securitate"), + ("Enable Audio", "Activează audio"), + ("Unlock Network Settings", "Deblochează setări de rețea"), + ("Server", "Server"), + ("Direct IP Access", "Acces direct IP"), + ("Proxy", "Proxy"), + ("Port", "Port"), + ("Apply", "Aplică"), + ("Disconnect all devices?", "Vrei să deconectezi toate dispozitivele?"), + ("Clear", "Golește"), + ("Audio Input Device", "Dispozitiv de intrare audio"), + ("Deny remote access", "Interzice acces la distanță"), + ("Use IP Whitelisting", "Folosește lista de IP-uri autorizate"), + ("Network", "Rețea"), + ("Enable RDP", "Activează RDP"), + ("Pin menubar", "Fixează bara de meniu"), + ("Unpin menubar", "Detașează bara de meniu"), + ("Recording", "Înregistrare"), + ("Directory", "Director"), + ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"), + ("Change", "Modifică"), + ("Start session recording", "Începe înregistrare"), + ("Stop session recording", "Oprește înregistrare"), + ("Enable Recording Session", "Activează înregistrarea sesiunii"), + ("Allow recording session", "Permite înregistrarea sesiunii"), + ("Enable LAN Discovery", "Activează descoperire LAN"), + ("Deny LAN Discovery", "Interzice descoperire LAN"), + ("Write a message", "Scrie un mesaj"), + ("Prompt", "Solicită"), + ("Please wait for confirmation of UAC...", "Așteaptă confirmarea UAC..."), + ("elevated_foreground_window_tip", "Fereastra actuală a dispozitivului la distanță necesită privilegii sporite pentru a funcționa, astfel că mouse-ul și tastatura nu pot fi folosite. Poți cere utilizatorului la distanță să minimizeze fereastra actuală sau să facă clic pe butonul de sporire a privilegiilor din fereastra de gestionare a conexiunilor. Pentru a evita această problemă, recomandăm instalarea software-ului pe dispozitivul la distanță."), + ("Disconnected", "Deconectat"), + ("Other", "Altele"), + ("Confirm before closing multiple tabs", "Confirmă înainte de a închide mai multe file"), + ("Keyboard Settings", "Configurare tastatură"), + ("Custom", "Personalizat"), + ("Full Access", "Acces total"), + ("Screen Share", "Partajare ecran"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), + ("JumpLink", "Afișează"), + ("Please Select the screen to be shared(Operate on the peer side).", "Partajează ecranul care urmează să fie partajat (operează din partea dispozitivului pereche)."), + ("Show RustDesk", "Afișează RustDesk"), + ("This PC", "Acest PC"), + ("or", "sau"), + ("Continue with", "Continuă cu"), + ("Elevate", "Sporește"), + ("Zoom cursor", "Cursor lupă"), + ("Accept sessions via password", "Acceptă sesiunile folosind parola"), + ("Accept sessions via click", "Acceptă sesiunile cu un clic de confirmare"), + ("Accept sessions via both", "Acceptă sesiunile folosind ambele moduri"), + ("Please wait for the remote side to accept your session request...", "Așteaptă ca solicitarea ta de conectare la distanță să fie acceptată..."), + ("One-time Password", "Parolă unică"), + ("Use one-time password", "Folosește parola unică"), + ("One-time password length", "Lungimea parolei unice"), + ("Request access to your device", "Solicită acces la dispozitivul tău"), + ("Hide connection management window", "Ascunde fereastra de gestionare a conexiunilor"), + ("hide_cm_tip", "Permite ascunderea ferestrei de gestionare doar dacă accepți începerea sesiunilor folosind parola permanentă"), + ].iter().cloned().collect(); +} From 917b3d22135c456a7a1f96dfdb0ab3a5f3ce5b2d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 26 Jan 2023 11:05:23 +0800 Subject: [PATCH 225/734] fix issue #2937 --- flutter/lib/models/server_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 176b1ba2d..7703182cd 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -323,10 +323,10 @@ class ServerModel with ChangeNotifier { notifyListeners(); parent.target?.ffiModel.updateEventListener(""); await parent.target?.invokeMethod("init_service"); + // ugly is here, because for desktop, below is useless await bind.mainStartService(); updateClientState(); - if (!Platform.isLinux) { - // current linux is not supported + if (Platform.isAndroid) { Wakelock.enable(); } } From cb5855a2730e41ae3c5c899a0182bc2059248253 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 26 Jan 2023 11:25:05 +0800 Subject: [PATCH 226/734] fix issue #2921 --- src/ui_interface.rs | 2 +- src/ui_session_interface.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ebaf8c317..403951eaa 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -236,7 +236,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - option_env!("RENDEZVOUS_SERVER").is_none() + option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 3fb3f2621..48f6c1090 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -6,10 +6,11 @@ use crate::client::{ }; use crate::common::{self, GrabState}; use crate::keyboard; +use crate::ui_interface::using_public_server; use crate::{client::Data, client::Interface}; use async_trait::async_trait; use bytes::Bytes; -use hbb_common::config::{Config, LocalConfig, PeerConfig}; +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::*}; @@ -835,6 +836,9 @@ pub async fn io_loop(handler: Session) { if key.is_empty() { key = crate::platform::get_license_key(); } + if key.is_empty() && !option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() { + key = RS_PUB_KEY.to_owned(); + } #[cfg(not(any(target_os = "android", target_os = "ios")))] if handler.is_port_forward() { if handler.is_rdp() { From a957edf93a6391cb570e129430b71d1ab3f69a15 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Thu, 26 Jan 2023 11:47:06 +0330 Subject: [PATCH 227/734] Update fa.rs ;-) --- src/lang/fa.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b107bb91a..da354579b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -186,7 +186,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logging in...", "...در حال ورود"), ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), ("Auto Login", "ورود خودکار"), - ("Enable Direct IP Access", "دسترسی مستقیم IP را فعال کنید"), + ("Enable Direct IP Access", "را فعال کنید IP دسترسی مستقیم"), ("Rename", "تغییر نام"), ("Space", "فضا"), ("Create Desktop Shortcut", "ساخت میانبر روی دسکتاپ"), @@ -349,7 +349,7 @@ 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?", "همه دستگاه ها قطع شوند؟"), From 06a0aeb03be3c610e06ab1010d84c8f71ce21643 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 26 Jan 2023 22:57:49 +0800 Subject: [PATCH 228/734] opt: upgrade flutter to 3.7.0 --- .github/workflows/flutter-ci.yml | 14 ++++++-------- .github/workflows/flutter-nightly.yml | 10 +++++----- flutter/macos/Runner/MainFlutterWindow.swift | 2 +- flutter/pubspec.yaml | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 4e98f311d..bcdad4a29 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -13,8 +13,7 @@ on: env: LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.7.0" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -51,9 +50,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -853,12 +852,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -887,7 +886,6 @@ jobs: # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH flutter-elinux doctor -v - # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b33a6dba0..4cb547aa4 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -9,7 +9,7 @@ on: env: LLVM_VERSION: "10.0" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.7.0" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility @@ -53,9 +53,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -1002,9 +1002,9 @@ jobs: # clone repo and reset to flutter 3.0.5 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 540cd9ab9..108f5a5f8 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -7,7 +7,7 @@ import desktop_drop import device_info_plus_macos import flutter_custom_cursor import package_info_plus_macos -import path_provider_macos +import path_provider_foundation import screen_retriever import sqflite // import tray_manager diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a5535c8b7..c2a5f1c02 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 ffi: ^2.0.1 - path_provider: ^2.0.2 + path_provider: ^2.0.12 external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 From db9afbcb01e98d73b77a1bdb6637fdffadb4198f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 26 Jan 2023 23:37:28 +0800 Subject: [PATCH 229/734] opt: do not show window when preparing --- flutter/lib/utils/multi_window_manager.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index ee19ac485..7914a4c0a 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -63,8 +63,7 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.RemoteDesktop)) - ..show(); + overrideType: WindowType.RemoteDesktop)); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -90,8 +89,7 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.FileTransfer)) - ..show(); + overrideType: WindowType.FileTransfer)); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -116,9 +114,8 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle( - getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) - ..show(); + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.PortForward)); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { From c9715c4e8748a9a9ac31a6516aea8eb268a4bdfe Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 26 Jan 2023 23:37:47 +0800 Subject: [PATCH 230/734] opt: remove unnecessary doc --- .github/workflows/flutter-nightly.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 4cb547aa4..151ff1213 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -8,7 +8,6 @@ on: env: LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.7.0" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 @@ -999,7 +998,7 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux # reset to flutter 3.7.0 From eac83fca28fb90cd3e4108854b2fa1bbb96c20d4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 27 Jan 2023 02:00:38 +0800 Subject: [PATCH 231/734] fix: update scrolling to fit flutter 3.3+ --- flutter/pubspec.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c2a5f1c02..0189ad9e4 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -75,14 +75,14 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^1.1.5 - flutter_improved_scrolling: ^0.0.3 + 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 + 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 From b144e28a60e7d7ba17434af1f7dcd436bda87b9a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 27 Jan 2023 02:34:28 +0800 Subject: [PATCH 232/734] fix: arm64 build on flutter 3.7.0 https://github.com/flutter/flutter/issues/116703#issuecomment-1403956612 --- .github/workflows/flutter-ci.yml | 11 +++++++++-- .github/workflows/flutter-nightly.yml | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index bcdad4a29..a2c67551d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -882,10 +882,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 151ff1213..896afd005 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1028,10 +1028,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) From a0cc71c86dafb6b2ea113c3841ae4365dbf9e639 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:08:33 +0100 Subject: [PATCH 233/734] Update fr.rs Some small fixes and improvements --- src/lang/fr.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ea2dbfede..9f1f23205 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -77,10 +77,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connected, waiting for image...", "Connecté, en attente de transmission d'image..."), ("Name", "Nom"), ("Type", "Type"), - ("Modified", "Modifié"), + ("Modified", "Modifié le"), ("Size", "Taille"), ("Show Hidden Files", "Afficher les fichiers cachés"), - ("Receive", "Accepter"), + ("Receive", "Recevoir"), ("Send", "Envoyer"), ("Refresh File", "Actualiser le fichier"), ("Local", "Local"), @@ -90,7 +90,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Confirm Delete", "Confirmer la suppression"), ("Delete", "Supprimer"), ("Properties", "Propriétés"), - ("Multi Select", "Choix multiple"), + ("Multi Select", "Sélection multiple"), ("Select All", "Tout sélectionner"), ("Unselect All", "Tout déselectionner"), ("Empty Directory", "Répertoire vide"), @@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "Exécuter sans installer"), ("Always connected via relay", "Forcer la connexion relais"), ("Always connect via relay", "Forcer la connexion relais"), - ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), + ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), ("Verify", "Vérifier"), ("Remember me", "Se souvenir de moi"), @@ -269,7 +269,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Chat", "Discuter"), ("Total", "Total"), ("items", "éléments"), - ("Selected", "Choisi"), + ("Selected", "Sélectionné"), ("Screen Capture", "Capture d'écran"), ("Input Control", "Contrôle de saisie"), ("Audio Capture", "Capture audio"), @@ -303,7 +303,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "en mode privé"), ("Out privacy mode", "hors mode de confidentialité"), ("Language", "Langue"), - ("Keep RustDesk background service", "Gardez le service RustDesk service arrière plan"), + ("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), ("Connection not allowed", "Connexion non autorisée"), @@ -356,14 +356,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clear", "Effacer"), ("Audio Input Device", "Périphérique source audio"), ("Deny remote access", "Interdir l'accès distant"), - ("Use IP Whitelisting", "Utiliser liste blanche d'IP"), + ("Use IP Whitelisting", "Utiliser une liste blanche d'IP"), ("Network", "Réseau"), ("Enable RDP", "Activer RDP"), ("Pin menubar", "Épingler la barre de menus"), ("Unpin menubar", "Détacher la barre de menu"), ("Recording", "Enregistrement"), ("Directory", "Répertoire"), - ("Automatically record incoming sessions", "Enregistrement automatique des session entrantes"), + ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"), ("Change", "Modifier"), ("Start session recording", "Commencer l'enregistrement"), ("Stop session recording", "Stopper l'enregistrement"), From a8c3499d7b932560492b1e0796abb1fb8ae86be7 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 27 Jan 2023 11:42:08 +0800 Subject: [PATCH 234/734] sync with rustdesk-server hbb_common --- libs/hbb_common/build.rs | 5 +- libs/hbb_common/protos/message.proto | 1248 +++++++++++----------- libs/hbb_common/src/bytes_codec.rs | 8 +- libs/hbb_common/src/compress.rs | 7 +- libs/hbb_common/src/config.rs | 128 +-- libs/hbb_common/src/fs.rs | 183 ++-- libs/hbb_common/src/lib.rs | 144 +-- libs/hbb_common/src/password_security.rs | 6 +- libs/hbb_common/src/platform/linux.rs | 8 +- libs/hbb_common/src/socket_client.rs | 13 +- libs/hbb_common/src/tcp.rs | 16 +- libs/hbb_common/src/udp.rs | 70 +- 12 files changed, 913 insertions(+), 923 deletions(-) diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs index bff0cfafc..fe0d31076 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -8,10 +8,7 @@ fn main() { .out_dir(out_dir) .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") - .customize( - protobuf_codegen::Customize::default() - .tokio_bytes(true) - ) + .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) .run() .expect("Codegen failed."); } diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 12d698045..b7965f237 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -1,624 +1,624 @@ -syntax = "proto3"; -package hbb; - -message EncodedVideoFrame { - bytes data = 1; - bool key = 2; - int64 pts = 3; -} - -message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } - -message RGB { bool compress = 1; } - -// planes data send directly in binary for better use arraybuffer on web -message YUV { - bool compress = 1; - int32 stride = 2; -} - -message VideoFrame { - oneof union { - EncodedVideoFrames vp9s = 6; - RGB rgb = 7; - YUV yuv = 8; - EncodedVideoFrames h264s = 10; - EncodedVideoFrames h265s = 11; - } - int64 timestamp = 9; -} - -message IdPk { - string id = 1; - bytes pk = 2; -} - -message DisplayInfo { - sint32 x = 1; - sint32 y = 2; - int32 width = 3; - int32 height = 4; - string name = 5; - bool online = 6; - bool cursor_embedded = 7; -} - -message PortForward { - string host = 1; - int32 port = 2; -} - -message FileTransfer { - string dir = 1; - bool show_hidden = 2; -} - -message LoginRequest { - string username = 1; - bytes password = 2; - string my_id = 4; - string my_name = 5; - OptionMessage option = 6; - oneof union { - FileTransfer file_transfer = 7; - PortForward port_forward = 8; - } - bool video_ack_required = 9; - uint64 session_id = 10; - string version = 11; -} - -message ChatMessage { string text = 1; } - -message Features { - bool privacy_mode = 1; -} - -message SupportedEncoding { - bool h264 = 1; - bool h265 = 2; -} - -message PeerInfo { - string username = 1; - string hostname = 2; - string platform = 3; - repeated DisplayInfo displays = 4; - int32 current_display = 5; - bool sas_enabled = 6; - string version = 7; - int32 conn_id = 8; - Features features = 9; - SupportedEncoding encoding = 10; -} - -message LoginResponse { - oneof union { - string error = 1; - PeerInfo peer_info = 2; - } -} - -message MouseEvent { - int32 mask = 1; - sint32 x = 2; - sint32 y = 3; - repeated ControlKey modifiers = 4; -} - -enum KeyboardMode{ - Legacy = 0; - Map = 1; - Translate = 2; - Auto = 3; -} - -enum ControlKey { - Unknown = 0; - Alt = 1; - Backspace = 2; - CapsLock = 3; - Control = 4; - Delete = 5; - DownArrow = 6; - End = 7; - Escape = 8; - F1 = 9; - F10 = 10; - F11 = 11; - F12 = 12; - F2 = 13; - F3 = 14; - F4 = 15; - F5 = 16; - F6 = 17; - F7 = 18; - F8 = 19; - F9 = 20; - Home = 21; - LeftArrow = 22; - /// meta key (also known as "windows"; "super"; and "command") - Meta = 23; - /// option key on macOS (alt key on Linux and Windows) - Option = 24; // deprecated, use Alt instead - PageDown = 25; - PageUp = 26; - Return = 27; - RightArrow = 28; - Shift = 29; - Space = 30; - Tab = 31; - UpArrow = 32; - Numpad0 = 33; - Numpad1 = 34; - Numpad2 = 35; - Numpad3 = 36; - Numpad4 = 37; - Numpad5 = 38; - Numpad6 = 39; - Numpad7 = 40; - Numpad8 = 41; - Numpad9 = 42; - Cancel = 43; - Clear = 44; - Menu = 45; // deprecated, use Alt instead - Pause = 46; - Kana = 47; - Hangul = 48; - Junja = 49; - Final = 50; - Hanja = 51; - Kanji = 52; - Convert = 53; - Select = 54; - Print = 55; - Execute = 56; - Snapshot = 57; - Insert = 58; - Help = 59; - Sleep = 60; - Separator = 61; - Scroll = 62; - NumLock = 63; - RWin = 64; - Apps = 65; - Multiply = 66; - Add = 67; - Subtract = 68; - Decimal = 69; - Divide = 70; - Equals = 71; - NumpadEnter = 72; - RShift = 73; - RControl = 74; - RAlt = 75; - CtrlAltDel = 100; - LockScreen = 101; -} - -message KeyEvent { - bool down = 1; - bool press = 2; - oneof union { - ControlKey control_key = 3; - uint32 chr = 4; - uint32 unicode = 5; - string seq = 6; - } - repeated ControlKey modifiers = 8; - KeyboardMode mode = 9; -} - -message CursorData { - uint64 id = 1; - sint32 hotx = 2; - sint32 hoty = 3; - int32 width = 4; - int32 height = 5; - bytes colors = 6; -} - -message CursorPosition { - sint32 x = 1; - sint32 y = 2; -} - -message Hash { - string salt = 1; - string challenge = 2; -} - -message Clipboard { - bool compress = 1; - bytes content = 2; -} - -enum FileType { - Dir = 0; - DirLink = 2; - DirDrive = 3; - File = 4; - FileLink = 5; -} - -message FileEntry { - FileType entry_type = 1; - string name = 2; - bool is_hidden = 3; - uint64 size = 4; - uint64 modified_time = 5; -} - -message FileDirectory { - int32 id = 1; - string path = 2; - repeated FileEntry entries = 3; -} - -message ReadDir { - string path = 1; - bool include_hidden = 2; -} - -message ReadAllFiles { - int32 id = 1; - string path = 2; - bool include_hidden = 3; -} - -message FileAction { - oneof union { - ReadDir read_dir = 1; - FileTransferSendRequest send = 2; - FileTransferReceiveRequest receive = 3; - FileDirCreate create = 4; - FileRemoveDir remove_dir = 5; - FileRemoveFile remove_file = 6; - ReadAllFiles all_files = 7; - FileTransferCancel cancel = 8; - FileTransferSendConfirmRequest send_confirm = 9; - } -} - -message FileTransferCancel { int32 id = 1; } - -message FileResponse { - oneof union { - FileDirectory dir = 1; - FileTransferBlock block = 2; - FileTransferError error = 3; - FileTransferDone done = 4; - FileTransferDigest digest = 5; - } -} - -message FileTransferDigest { - int32 id = 1; - sint32 file_num = 2; - uint64 last_modified = 3; - uint64 file_size = 4; - bool is_upload = 5; -} - -message FileTransferBlock { - int32 id = 1; - sint32 file_num = 2; - bytes data = 3; - bool compressed = 4; - uint32 blk_id = 5; -} - -message FileTransferError { - int32 id = 1; - string error = 2; - sint32 file_num = 3; -} - -message FileTransferSendRequest { - int32 id = 1; - string path = 2; - bool include_hidden = 3; - int32 file_num = 4; -} - -message FileTransferSendConfirmRequest { - int32 id = 1; - sint32 file_num = 2; - oneof union { - bool skip = 3; - uint32 offset_blk = 4; - } -} - -message FileTransferDone { - int32 id = 1; - sint32 file_num = 2; -} - -message FileTransferReceiveRequest { - int32 id = 1; - string path = 2; // path written to - repeated FileEntry files = 3; - int32 file_num = 4; -} - -message FileRemoveDir { - int32 id = 1; - string path = 2; - bool recursive = 3; -} - -message FileRemoveFile { - int32 id = 1; - string path = 2; - sint32 file_num = 3; -} - -message FileDirCreate { - int32 id = 1; - string path = 2; -} - -// main logic from freeRDP -message CliprdrMonitorReady { -} - -message CliprdrFormat { - int32 id = 2; - string format = 3; -} - -message CliprdrServerFormatList { - repeated CliprdrFormat formats = 2; -} - -message CliprdrServerFormatListResponse { - int32 msg_flags = 2; -} - -message CliprdrServerFormatDataRequest { - int32 requested_format_id = 2; -} - -message CliprdrServerFormatDataResponse { - int32 msg_flags = 2; - bytes format_data = 3; -} - -message CliprdrFileContentsRequest { - int32 stream_id = 2; - int32 list_index = 3; - int32 dw_flags = 4; - int32 n_position_low = 5; - int32 n_position_high = 6; - int32 cb_requested = 7; - bool have_clip_data_id = 8; - int32 clip_data_id = 9; -} - -message CliprdrFileContentsResponse { - int32 msg_flags = 3; - int32 stream_id = 4; - bytes requested_data = 5; -} - -message Cliprdr { - oneof union { - CliprdrMonitorReady ready = 1; - CliprdrServerFormatList format_list = 2; - CliprdrServerFormatListResponse format_list_response = 3; - CliprdrServerFormatDataRequest format_data_request = 4; - CliprdrServerFormatDataResponse format_data_response = 5; - CliprdrFileContentsRequest file_contents_request = 6; - CliprdrFileContentsResponse file_contents_response = 7; - } -} - -message SwitchDisplay { - int32 display = 1; - sint32 x = 2; - sint32 y = 3; - int32 width = 4; - int32 height = 5; - bool cursor_embedded = 6; -} - -message PermissionInfo { - enum Permission { - Keyboard = 0; - Clipboard = 2; - Audio = 3; - File = 4; - Restart = 5; - Recording = 6; - } - - Permission permission = 1; - bool enabled = 2; -} - -enum ImageQuality { - NotSet = 0; - Low = 2; - Balanced = 3; - Best = 4; -} - -message VideoCodecState { - enum PreferCodec { - Auto = 0; - VPX = 1; - H264 = 2; - H265 = 3; - } - - int32 score_vpx = 1; - int32 score_h264 = 2; - int32 score_h265 = 3; - PreferCodec prefer = 4; -} - -message OptionMessage { - enum BoolOption { - NotSet = 0; - No = 1; - Yes = 2; - } - ImageQuality image_quality = 1; - BoolOption lock_after_session_end = 2; - BoolOption show_remote_cursor = 3; - BoolOption privacy_mode = 4; - BoolOption block_input = 5; - int32 custom_image_quality = 6; - BoolOption disable_audio = 7; - BoolOption disable_clipboard = 8; - BoolOption enable_file_transfer = 9; - VideoCodecState video_codec_state = 10; - int32 custom_fps = 11; -} - -message TestDelay { - int64 time = 1; - bool from_client = 2; - uint32 last_delay = 3; - uint32 target_bitrate = 4; -} - -message PublicKey { - bytes asymmetric_value = 1; - bytes symmetric_value = 2; -} - -message SignedId { bytes id = 1; } - -message AudioFormat { - uint32 sample_rate = 1; - uint32 channels = 2; -} - -message AudioFrame { - bytes data = 1; - int64 timestamp = 2; -} - -// Notify peer to show message box. -message MessageBox { - // Message type. Refer to flutter/lib/common.dart/msgBox(). - string msgtype = 1; - string title = 2; - // English - string text = 3; - // If not empty, msgbox provides a button to following the link. - // The link here can't be directly http url. - // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). - string link = 4; -} - -message BackNotification { - // no need to consider block input by someone else - enum BlockInputState { - BlkStateUnknown = 0; - BlkOnSucceeded = 2; - BlkOnFailed = 3; - BlkOffSucceeded = 4; - BlkOffFailed = 5; - } - enum PrivacyModeState { - PrvStateUnknown = 0; - // Privacy mode on by someone else - PrvOnByOther = 2; - // Privacy mode is not supported on the remote side - PrvNotSupported = 3; - // Privacy mode on by self - PrvOnSucceeded = 4; - // Privacy mode on by self, but denied - PrvOnFailedDenied = 5; - // Some plugins are not found - PrvOnFailedPlugin = 6; - // Privacy mode on by self, but failed - PrvOnFailed = 7; - // Privacy mode off by self - PrvOffSucceeded = 8; - // Ctrl + P - PrvOffByPeer = 9; - // Privacy mode off by self, but failed - PrvOffFailed = 10; - PrvOffUnknown = 11; - } - - oneof union { - PrivacyModeState privacy_mode_state = 1; - BlockInputState block_input_state = 2; - } -} - -message ElevationRequestWithLogon { - string username = 1; - string password = 2; -} - -message ElevationRequest { - oneof union { - bool direct = 1; - ElevationRequestWithLogon logon = 2; - } -} - -message SwitchSidesRequest { - bytes uuid = 1; -} - -message SwitchSidesResponse { - bytes uuid = 1; - LoginRequest lr = 2; -} - -message SwitchBack {} - -message Misc { - oneof union { - ChatMessage chat_message = 4; - SwitchDisplay switch_display = 5; - PermissionInfo permission_info = 6; - OptionMessage option = 7; - AudioFormat audio_format = 8; - string close_reason = 9; - bool refresh_video = 10; - bool video_received = 12; - BackNotification back_notification = 13; - bool restart_remote_device = 14; - bool uac = 15; - bool foreground_window_elevated = 16; - bool stop_service = 17; - ElevationRequest elevation_request = 18; - string elevation_response = 19; - bool portable_service_running = 20; - SwitchSidesRequest switch_sides_request = 21; - SwitchBack switch_back = 22; - } -} - -message Message { - oneof union { - SignedId signed_id = 3; - PublicKey public_key = 4; - TestDelay test_delay = 5; - VideoFrame video_frame = 6; - LoginRequest login_request = 7; - LoginResponse login_response = 8; - Hash hash = 9; - MouseEvent mouse_event = 10; - AudioFrame audio_frame = 11; - CursorData cursor_data = 12; - CursorPosition cursor_position = 13; - uint64 cursor_id = 14; - KeyEvent key_event = 15; - Clipboard clipboard = 16; - FileAction file_action = 17; - FileResponse file_response = 18; - Misc misc = 19; - Cliprdr cliprdr = 20; - MessageBox message_box = 21; - SwitchSidesResponse switch_sides_response = 22; - } -} +syntax = "proto3"; +package hbb; + +message EncodedVideoFrame { + bytes data = 1; + bool key = 2; + int64 pts = 3; +} + +message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } + +message RGB { bool compress = 1; } + +// planes data send directly in binary for better use arraybuffer on web +message YUV { + bool compress = 1; + int32 stride = 2; +} + +message VideoFrame { + oneof union { + EncodedVideoFrames vp9s = 6; + RGB rgb = 7; + YUV yuv = 8; + EncodedVideoFrames h264s = 10; + EncodedVideoFrames h265s = 11; + } + int64 timestamp = 9; +} + +message IdPk { + string id = 1; + bytes pk = 2; +} + +message DisplayInfo { + sint32 x = 1; + sint32 y = 2; + int32 width = 3; + int32 height = 4; + string name = 5; + bool online = 6; + bool cursor_embedded = 7; +} + +message PortForward { + string host = 1; + int32 port = 2; +} + +message FileTransfer { + string dir = 1; + bool show_hidden = 2; +} + +message LoginRequest { + string username = 1; + bytes password = 2; + string my_id = 4; + string my_name = 5; + OptionMessage option = 6; + oneof union { + FileTransfer file_transfer = 7; + PortForward port_forward = 8; + } + bool video_ack_required = 9; + uint64 session_id = 10; + string version = 11; +} + +message ChatMessage { string text = 1; } + +message Features { + bool privacy_mode = 1; +} + +message SupportedEncoding { + bool h264 = 1; + bool h265 = 2; +} + +message PeerInfo { + string username = 1; + string hostname = 2; + string platform = 3; + repeated DisplayInfo displays = 4; + int32 current_display = 5; + bool sas_enabled = 6; + string version = 7; + int32 conn_id = 8; + Features features = 9; + SupportedEncoding encoding = 10; +} + +message LoginResponse { + oneof union { + string error = 1; + PeerInfo peer_info = 2; + } +} + +message MouseEvent { + int32 mask = 1; + sint32 x = 2; + sint32 y = 3; + repeated ControlKey modifiers = 4; +} + +enum KeyboardMode{ + Legacy = 0; + Map = 1; + Translate = 2; + Auto = 3; +} + +enum ControlKey { + Unknown = 0; + Alt = 1; + Backspace = 2; + CapsLock = 3; + Control = 4; + Delete = 5; + DownArrow = 6; + End = 7; + Escape = 8; + F1 = 9; + F10 = 10; + F11 = 11; + F12 = 12; + F2 = 13; + F3 = 14; + F4 = 15; + F5 = 16; + F6 = 17; + F7 = 18; + F8 = 19; + F9 = 20; + Home = 21; + LeftArrow = 22; + /// meta key (also known as "windows"; "super"; and "command") + Meta = 23; + /// option key on macOS (alt key on Linux and Windows) + Option = 24; // deprecated, use Alt instead + PageDown = 25; + PageUp = 26; + Return = 27; + RightArrow = 28; + Shift = 29; + Space = 30; + Tab = 31; + UpArrow = 32; + Numpad0 = 33; + Numpad1 = 34; + Numpad2 = 35; + Numpad3 = 36; + Numpad4 = 37; + Numpad5 = 38; + Numpad6 = 39; + Numpad7 = 40; + Numpad8 = 41; + Numpad9 = 42; + Cancel = 43; + Clear = 44; + Menu = 45; // deprecated, use Alt instead + Pause = 46; + Kana = 47; + Hangul = 48; + Junja = 49; + Final = 50; + Hanja = 51; + Kanji = 52; + Convert = 53; + Select = 54; + Print = 55; + Execute = 56; + Snapshot = 57; + Insert = 58; + Help = 59; + Sleep = 60; + Separator = 61; + Scroll = 62; + NumLock = 63; + RWin = 64; + Apps = 65; + Multiply = 66; + Add = 67; + Subtract = 68; + Decimal = 69; + Divide = 70; + Equals = 71; + NumpadEnter = 72; + RShift = 73; + RControl = 74; + RAlt = 75; + CtrlAltDel = 100; + LockScreen = 101; +} + +message KeyEvent { + bool down = 1; + bool press = 2; + oneof union { + ControlKey control_key = 3; + uint32 chr = 4; + uint32 unicode = 5; + string seq = 6; + } + repeated ControlKey modifiers = 8; + KeyboardMode mode = 9; +} + +message CursorData { + uint64 id = 1; + sint32 hotx = 2; + sint32 hoty = 3; + int32 width = 4; + int32 height = 5; + bytes colors = 6; +} + +message CursorPosition { + sint32 x = 1; + sint32 y = 2; +} + +message Hash { + string salt = 1; + string challenge = 2; +} + +message Clipboard { + bool compress = 1; + bytes content = 2; +} + +enum FileType { + Dir = 0; + DirLink = 2; + DirDrive = 3; + File = 4; + FileLink = 5; +} + +message FileEntry { + FileType entry_type = 1; + string name = 2; + bool is_hidden = 3; + uint64 size = 4; + uint64 modified_time = 5; +} + +message FileDirectory { + int32 id = 1; + string path = 2; + repeated FileEntry entries = 3; +} + +message ReadDir { + string path = 1; + bool include_hidden = 2; +} + +message ReadAllFiles { + int32 id = 1; + string path = 2; + bool include_hidden = 3; +} + +message FileAction { + oneof union { + ReadDir read_dir = 1; + FileTransferSendRequest send = 2; + FileTransferReceiveRequest receive = 3; + FileDirCreate create = 4; + FileRemoveDir remove_dir = 5; + FileRemoveFile remove_file = 6; + ReadAllFiles all_files = 7; + FileTransferCancel cancel = 8; + FileTransferSendConfirmRequest send_confirm = 9; + } +} + +message FileTransferCancel { int32 id = 1; } + +message FileResponse { + oneof union { + FileDirectory dir = 1; + FileTransferBlock block = 2; + FileTransferError error = 3; + FileTransferDone done = 4; + FileTransferDigest digest = 5; + } +} + +message FileTransferDigest { + int32 id = 1; + sint32 file_num = 2; + uint64 last_modified = 3; + uint64 file_size = 4; + bool is_upload = 5; +} + +message FileTransferBlock { + int32 id = 1; + sint32 file_num = 2; + bytes data = 3; + bool compressed = 4; + uint32 blk_id = 5; +} + +message FileTransferError { + int32 id = 1; + string error = 2; + sint32 file_num = 3; +} + +message FileTransferSendRequest { + int32 id = 1; + string path = 2; + bool include_hidden = 3; + int32 file_num = 4; +} + +message FileTransferSendConfirmRequest { + int32 id = 1; + sint32 file_num = 2; + oneof union { + bool skip = 3; + uint32 offset_blk = 4; + } +} + +message FileTransferDone { + int32 id = 1; + sint32 file_num = 2; +} + +message FileTransferReceiveRequest { + int32 id = 1; + string path = 2; // path written to + repeated FileEntry files = 3; + int32 file_num = 4; +} + +message FileRemoveDir { + int32 id = 1; + string path = 2; + bool recursive = 3; +} + +message FileRemoveFile { + int32 id = 1; + string path = 2; + sint32 file_num = 3; +} + +message FileDirCreate { + int32 id = 1; + string path = 2; +} + +// main logic from freeRDP +message CliprdrMonitorReady { +} + +message CliprdrFormat { + int32 id = 2; + string format = 3; +} + +message CliprdrServerFormatList { + repeated CliprdrFormat formats = 2; +} + +message CliprdrServerFormatListResponse { + int32 msg_flags = 2; +} + +message CliprdrServerFormatDataRequest { + int32 requested_format_id = 2; +} + +message CliprdrServerFormatDataResponse { + int32 msg_flags = 2; + bytes format_data = 3; +} + +message CliprdrFileContentsRequest { + int32 stream_id = 2; + int32 list_index = 3; + int32 dw_flags = 4; + int32 n_position_low = 5; + int32 n_position_high = 6; + int32 cb_requested = 7; + bool have_clip_data_id = 8; + int32 clip_data_id = 9; +} + +message CliprdrFileContentsResponse { + int32 msg_flags = 3; + int32 stream_id = 4; + bytes requested_data = 5; +} + +message Cliprdr { + oneof union { + CliprdrMonitorReady ready = 1; + CliprdrServerFormatList format_list = 2; + CliprdrServerFormatListResponse format_list_response = 3; + CliprdrServerFormatDataRequest format_data_request = 4; + CliprdrServerFormatDataResponse format_data_response = 5; + CliprdrFileContentsRequest file_contents_request = 6; + CliprdrFileContentsResponse file_contents_response = 7; + } +} + +message SwitchDisplay { + int32 display = 1; + sint32 x = 2; + sint32 y = 3; + int32 width = 4; + int32 height = 5; + bool cursor_embedded = 6; +} + +message PermissionInfo { + enum Permission { + Keyboard = 0; + Clipboard = 2; + Audio = 3; + File = 4; + Restart = 5; + Recording = 6; + } + + Permission permission = 1; + bool enabled = 2; +} + +enum ImageQuality { + NotSet = 0; + Low = 2; + Balanced = 3; + Best = 4; +} + +message VideoCodecState { + enum PreferCodec { + Auto = 0; + VPX = 1; + H264 = 2; + H265 = 3; + } + + int32 score_vpx = 1; + int32 score_h264 = 2; + int32 score_h265 = 3; + PreferCodec prefer = 4; +} + +message OptionMessage { + enum BoolOption { + NotSet = 0; + No = 1; + Yes = 2; + } + ImageQuality image_quality = 1; + BoolOption lock_after_session_end = 2; + BoolOption show_remote_cursor = 3; + BoolOption privacy_mode = 4; + BoolOption block_input = 5; + int32 custom_image_quality = 6; + BoolOption disable_audio = 7; + BoolOption disable_clipboard = 8; + BoolOption enable_file_transfer = 9; + VideoCodecState video_codec_state = 10; + int32 custom_fps = 11; +} + +message TestDelay { + int64 time = 1; + bool from_client = 2; + uint32 last_delay = 3; + uint32 target_bitrate = 4; +} + +message PublicKey { + bytes asymmetric_value = 1; + bytes symmetric_value = 2; +} + +message SignedId { bytes id = 1; } + +message AudioFormat { + uint32 sample_rate = 1; + uint32 channels = 2; +} + +message AudioFrame { + bytes data = 1; + int64 timestamp = 2; +} + +// Notify peer to show message box. +message MessageBox { + // Message type. Refer to flutter/lib/common.dart/msgBox(). + string msgtype = 1; + string title = 2; + // English + string text = 3; + // If not empty, msgbox provides a button to following the link. + // The link here can't be directly http url. + // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). + string link = 4; +} + +message BackNotification { + // no need to consider block input by someone else + enum BlockInputState { + BlkStateUnknown = 0; + BlkOnSucceeded = 2; + BlkOnFailed = 3; + BlkOffSucceeded = 4; + BlkOffFailed = 5; + } + enum PrivacyModeState { + PrvStateUnknown = 0; + // Privacy mode on by someone else + PrvOnByOther = 2; + // Privacy mode is not supported on the remote side + PrvNotSupported = 3; + // Privacy mode on by self + PrvOnSucceeded = 4; + // Privacy mode on by self, but denied + PrvOnFailedDenied = 5; + // Some plugins are not found + PrvOnFailedPlugin = 6; + // Privacy mode on by self, but failed + PrvOnFailed = 7; + // Privacy mode off by self + PrvOffSucceeded = 8; + // Ctrl + P + PrvOffByPeer = 9; + // Privacy mode off by self, but failed + PrvOffFailed = 10; + PrvOffUnknown = 11; + } + + oneof union { + PrivacyModeState privacy_mode_state = 1; + BlockInputState block_input_state = 2; + } +} + +message ElevationRequestWithLogon { + string username = 1; + string password = 2; +} + +message ElevationRequest { + oneof union { + bool direct = 1; + ElevationRequestWithLogon logon = 2; + } +} + +message SwitchSidesRequest { + bytes uuid = 1; +} + +message SwitchSidesResponse { + bytes uuid = 1; + LoginRequest lr = 2; +} + +message SwitchBack {} + +message Misc { + oneof union { + ChatMessage chat_message = 4; + SwitchDisplay switch_display = 5; + PermissionInfo permission_info = 6; + OptionMessage option = 7; + AudioFormat audio_format = 8; + string close_reason = 9; + bool refresh_video = 10; + bool video_received = 12; + BackNotification back_notification = 13; + bool restart_remote_device = 14; + bool uac = 15; + bool foreground_window_elevated = 16; + bool stop_service = 17; + ElevationRequest elevation_request = 18; + string elevation_response = 19; + bool portable_service_running = 20; + SwitchSidesRequest switch_sides_request = 21; + SwitchBack switch_back = 22; + } +} + +message Message { + oneof union { + SignedId signed_id = 3; + PublicKey public_key = 4; + TestDelay test_delay = 5; + VideoFrame video_frame = 6; + LoginRequest login_request = 7; + LoginResponse login_response = 8; + Hash hash = 9; + MouseEvent mouse_event = 10; + AudioFrame audio_frame = 11; + CursorData cursor_data = 12; + CursorPosition cursor_position = 13; + uint64 cursor_id = 14; + KeyEvent key_event = 15; + Clipboard clipboard = 16; + FileAction file_action = 17; + FileResponse file_response = 18; + Misc misc = 19; + Cliprdr cliprdr = 20; + MessageBox message_box = 21; + SwitchSidesResponse switch_sides_response = 22; + } +} diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs index e029f1cc0..699aa9bff 100644 --- a/libs/hbb_common/src/bytes_codec.rs +++ b/libs/hbb_common/src/bytes_codec.rs @@ -15,6 +15,12 @@ enum DecodeState { Data(usize), } +impl Default for BytesCodec { + fn default() -> Self { + Self::new() + } +} + impl BytesCodec { pub fn new() -> Self { Self { @@ -56,7 +62,7 @@ impl BytesCodec { } src.advance(head_len); src.reserve(n); - return Ok(Some(n)); + Ok(Some(n)) } fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result> { diff --git a/libs/hbb_common/src/compress.rs b/libs/hbb_common/src/compress.rs index a969ccf86..e7668a949 100644 --- a/libs/hbb_common/src/compress.rs +++ b/libs/hbb_common/src/compress.rs @@ -32,12 +32,7 @@ pub fn decompress(data: &[u8]) -> Vec { const MAX: usize = 1024 * 1024 * 64; const MIN: usize = 1024 * 1024; let mut n = 30 * data.len(); - if n > MAX { - n = MAX; - } - if n < MIN { - n = MIN; - } + n = n.clamp(MIN, MAX); match d.decompress(data, n) { Ok(res) => out = res, Err(err) => { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 20334ed12..8bea99106 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -29,7 +29,7 @@ pub const READ_TIMEOUT: u64 = 30_000; pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; -const PASSWORD_ENC_VERSION: &'static str = "00"; +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= @@ -43,6 +43,7 @@ lazy_static::lazy_static! { } type Size = (i32, i32, i32, i32); +type KeyPair = (Vec, Vec); lazy_static::lazy_static! { static ref CONFIG: Arc> = Arc::new(RwLock::new(Config::load())); @@ -54,7 +55,7 @@ lazy_static::lazy_static! { _ => "", }.to_owned())); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); - static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); + static ref KEY_PAIR: Arc>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); } @@ -75,18 +76,18 @@ lazy_static::lazy_static! { ]); } -const CHARS: &'static [char] = &[ +const CHARS: &[char] = &[ '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ +pub const RENDEZVOUS_SERVERS: &[&str] = &[ "rs-ny.rustdesk.com", "rs-sg.rustdesk.com", "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { +pub const RS_PUB_KEY: &str = match option_env!("RS_PUB_KEY") { Some(key) if !key.is_empty() => key, _ => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", }; @@ -131,7 +132,7 @@ pub struct Config { #[serde(default)] salt: String, #[serde(default)] - key_pair: (Vec, Vec), // sk, pk + key_pair: KeyPair, // sk, pk #[serde(default)] key_confirmed: bool, #[serde(default)] @@ -319,7 +320,7 @@ impl Config2 { pub fn load_path( file: PathBuf, ) -> T { - let cfg = match confy::load_path(&file) { + let cfg = match confy::load_path(file) { Ok(config) => config, Err(err) => { log::error!("Failed to load config: {}", err); @@ -366,20 +367,16 @@ impl Config { config.id = id; id_valid = true; store |= store2; - } else { - if crate::get_modified_time(&Self::file_("")) - .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation - .unwrap_or(crate::get_exe_time()) - < crate::get_exe_time() - { - if !config.id.is_empty() - && config.enc_id.is_empty() - && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 - { - id_valid = true; - store = true; - } - } + } else if crate::get_modified_time(&Self::file_("")) + .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation + .unwrap_or_else(crate::get_exe_time) + < crate::get_exe_time() + && !config.id.is_empty() + && config.enc_id.is_empty() + && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 + { + id_valid = true; + store = true; } if !id_valid { for _ in 0..3 { @@ -444,18 +441,18 @@ impl Config { #[cfg(not(any(target_os = "android", target_os = "ios")))] { #[cfg(not(target_os = "macos"))] - let org = ""; + let org = "".to_owned(); #[cfg(target_os = "macos")] let org = ORG.read().unwrap().clone(); // /var/root for root if let Some(project) = - directories_next::ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) + directories_next::ProjectDirs::from("", &org, &APP_NAME.read().unwrap()) { let mut path = patch(project.config_dir().to_path_buf()); path.push(p); return path; } - return "".into(); + "".into() } } @@ -539,9 +536,9 @@ impl Config { rendezvous_server = Self::get_rendezvous_servers() .drain(..) .next() - .unwrap_or("".to_owned()); + .unwrap_or_default(); } - if !rendezvous_server.contains(":") { + if !rendezvous_server.contains(':') { rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT); } rendezvous_server @@ -559,8 +556,8 @@ impl Config { let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; if serial_obsolute { let ss: Vec = Self::get_option("rendezvous-servers") - .split(",") - .filter(|x| x.contains(".")) + .split(',') + .filter(|x| x.contains('.')) .map(|x| x.to_owned()) .collect(); if !ss.is_empty() { @@ -580,7 +577,7 @@ impl Config { let mut delay = i64::MAX; for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() { if tmp_delay > &0 && tmp_delay < &delay { - delay = tmp_delay.clone(); + delay = *tmp_delay; host = tmp_host.to_string(); } } @@ -647,7 +644,7 @@ impl Config { for x in &ma.bytes()[2..] { id = (id << 8) | (*x as u32); } - id = id & 0x1FFFFFFF; + id &= 0x1FFFFFFF; Some(id.to_string()) } else { None @@ -679,11 +676,7 @@ impl Config { } pub fn get_host_key_confirmed(host: &str) -> bool { - if let Some(true) = CONFIG.read().unwrap().keys_confirmed.get(host) { - true - } else { - false - } + matches!(CONFIG.read().unwrap().keys_confirmed.get(host), Some(true)) } pub fn set_host_key_confirmed(host: &str, v: bool) { @@ -695,7 +688,7 @@ impl Config { config.store(); } - pub fn get_key_pair() -> (Vec, Vec) { + pub fn get_key_pair() -> KeyPair { // lock here to make sure no gen_keypair more than once // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function let mut lock = KEY_PAIR.lock().unwrap(); @@ -714,7 +707,7 @@ impl Config { }); } *lock = Some(config.key_pair.clone()); - return config.key_pair; + config.key_pair } pub fn get_id() -> String { @@ -849,7 +842,7 @@ impl Config { let ext = path.extension(); if let Some(ext) = ext { let ext = format!("{}.toml", ext.to_string_lossy()); - path.with_extension(&ext) + path.with_extension(ext) } else { path.with_extension("toml") } @@ -861,7 +854,7 @@ const PEERS: &str = "peers"; impl PeerConfig { pub fn load(id: &str) -> PeerConfig { let _lock = CONFIG.read().unwrap(); - match confy::load_path(&Self::path(id)) { + match confy::load_path(Self::path(id)) { Ok(config) => { let mut config: PeerConfig = config; let mut store = false; @@ -869,16 +862,16 @@ impl PeerConfig { decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); config.password = password; store = store || store2; - config.options.get_mut("rdp_password").map(|v| { + if let Some(v) = config.options.get_mut("rdp_password") { let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); *v = password; store = store || store2; - }); - config.options.get_mut("os-password").map(|v| { + } + if let Some(v) = config.options.get_mut("os-password") { let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); *v = password; store = store || store2; - }); + } if store { config.store(id); } @@ -895,34 +888,29 @@ impl PeerConfig { let _lock = CONFIG.read().unwrap(); let mut config = self.clone(); config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); - config - .options - .get_mut("rdp_password") - .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)); - config - .options - .get_mut("os-password") - .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)); + if let Some(v) = config.options.get_mut("rdp_password") { + *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) + } + if let Some(v) = config.options.get_mut("os-password") { + *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) + }; if let Err(err) = store_path(Self::path(id), config) { log::error!("Failed to store config: {}", err); } } pub fn remove(id: &str) { - fs::remove_file(&Self::path(id)).ok(); + fs::remove_file(Self::path(id)).ok(); } fn path(id: &str) -> PathBuf { - let id_encoded: String; - //If the id contains invalid chars, encode it let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*").unwrap(); - if forbidden_paths.is_match(id) { - id_encoded = - "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str(); + let id_encoded = if forbidden_paths.is_match(id) { + "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str() } else { - id_encoded = id.to_string(); - } + id.to_string() + }; let path: PathBuf = [PEERS, id_encoded.as_str()].iter().collect(); Config::with_extension(Config::path(path)) } @@ -940,26 +928,24 @@ impl PeerConfig { && p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml") }) .map(|p| { - let t = crate::get_modified_time(&p); + let t = crate::get_modified_time(p); let id = p .file_stem() .map(|p| p.to_str().unwrap_or("")) .unwrap_or("") .to_owned(); - let id_decoded_string: String; - if id.starts_with("base64_") && id.len() != 7 { + let id_decoded_string = if id.starts_with("base64_") && id.len() != 7 { let id_decoded = base64::decode(&id[7..], base64::Variant::Original) - .unwrap_or(Vec::new()); - id_decoded_string = - String::from_utf8_lossy(&id_decoded).as_ref().to_owned(); + .unwrap_or_default(); + String::from_utf8_lossy(&id_decoded).as_ref().to_owned() } else { - id_decoded_string = id; - } + id + }; let c = PeerConfig::load(&id_decoded_string); if c.info.platform.is_empty() { - fs::remove_file(&p).ok(); + fs::remove_file(p).ok(); } (id_decoded_string, t, c) }) @@ -1149,7 +1135,7 @@ pub struct LanPeers { impl LanPeers { pub fn load() -> LanPeers { let _lock = CONFIG.read().unwrap(); - match confy::load_path(&Config::file_("_lan_peers")) { + match confy::load_path(Config::file_("_lan_peers")) { Ok(peers) => peers, Err(err) => { log::error!("Failed to load lan peers: {}", err); @@ -1158,9 +1144,9 @@ impl LanPeers { } } - pub fn store(peers: &Vec) { + pub fn store(peers: &[DiscoveryPeer]) { let f = LanPeers { - peers: peers.clone(), + peers: peers.to_owned(), }; if let Err(err) = store_path(Config::file_("_lan_peers"), f) { log::error!("Failed to store lan peers: {}", err); diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index fec8b8670..ea54e113a 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -13,13 +13,13 @@ use crate::{ config::{Config, COMPRESS_LEVEL}, }; -pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType { +pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType { let mut dir = FileDirectory { - path: get_string(&path), + path: get_string(path), ..Default::default() }; #[cfg(windows)] - if "/" == &get_string(&path) { + if "/" == &get_string(path) { let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() }; for i in 0..32 { if drives & (1 << i) != 0 { @@ -36,74 +36,70 @@ pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType String { +pub fn get_file_name(p: &Path) -> String { p.file_name() .map(|p| p.to_str().unwrap_or("")) .unwrap_or("") @@ -111,7 +107,7 @@ pub fn get_file_name(p: &PathBuf) -> String { } #[inline] -pub fn get_string(path: &PathBuf) -> String { +pub fn get_string(path: &Path) -> String { path.to_str().unwrap_or("").to_owned() } @@ -127,14 +123,14 @@ pub fn get_home_as_string() -> String { fn read_dir_recursive( path: &PathBuf, - prefix: &PathBuf, + prefix: &Path, include_hidden: bool, ) -> ResultType> { let mut files = Vec::new(); if path.is_dir() { // to-do: symbol link handling, cp the link rather than the content // to-do: file mode, for unix - let fd = read_dir(&path, include_hidden)?; + let fd = read_dir(path, include_hidden)?; for entry in fd.entries.iter() { match entry.entry_type.enum_value() { Ok(FileType::File) => { @@ -158,7 +154,7 @@ fn read_dir_recursive( } Ok(files) } else if path.is_file() { - let (size, modified_time) = if let Ok(meta) = std::fs::metadata(&path) { + let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) { ( meta.len(), meta.modified() @@ -167,7 +163,7 @@ fn read_dir_recursive( .map(|x| x.as_secs()) .unwrap_or(0) }) - .unwrap_or(0) as u64, + .unwrap_or(0), ) } else { (0, 0) @@ -249,7 +245,7 @@ pub struct RemoveJobMeta { #[inline] fn get_ext(name: &str) -> &str { - if let Some(i) = name.rfind(".") { + if let Some(i) = name.rfind('.') { return &name[i + 1..]; } "" @@ -270,6 +266,7 @@ fn is_compressed_file(name: &str) -> bool { } impl TransferJob { + #[allow(clippy::too_many_arguments)] pub fn new_write( id: i32, remote: String, @@ -281,7 +278,7 @@ impl TransferJob { enable_overwrite_detection: bool, ) -> Self { log::info!("new write {}", path); - let total_size = files.iter().map(|x| x.size as u64).sum(); + let total_size = files.iter().map(|x| x.size).sum(); Self { id, remote, @@ -307,7 +304,7 @@ impl TransferJob { ) -> ResultType { log::info!("new read {}", path); let files = get_recursive_files(&path, show_hidden)?; - let total_size = files.iter().map(|x| x.size as u64).sum(); + let total_size = files.iter().map(|x| x.size).sum(); Ok(Self { id, remote, @@ -363,7 +360,7 @@ impl TransferJob { let entry = &self.files[file_num]; let path = self.join(&entry.name); let download_path = format!("{}.download", get_string(&path)); - std::fs::rename(&download_path, &path).ok(); + std::fs::rename(download_path, &path).ok(); filetime::set_file_mtime( &path, filetime::FileTime::from_unix_time(entry.modified_time as _, 0), @@ -378,7 +375,7 @@ impl TransferJob { let entry = &self.files[file_num]; let path = self.join(&entry.name); let download_path = format!("{}.download", get_string(&path)); - std::fs::remove_file(&download_path).ok(); + std::fs::remove_file(download_path).ok(); } } @@ -433,7 +430,7 @@ impl TransferJob { } let name = &self.files[file_num].name; if self.file.is_none() { - match File::open(self.join(&name)).await { + match File::open(self.join(name)).await { Ok(file) => { self.file = Some(file); self.file_confirmed = false; @@ -447,20 +444,15 @@ impl TransferJob { } } } - if self.enable_overwrite_detection { - if !self.file_confirmed() { - if !self.file_is_waiting() { - self.send_current_digest(stream).await?; - self.set_file_is_waiting(true); - } - return Ok(None); + if self.enable_overwrite_detection && !self.file_confirmed() { + if !self.file_is_waiting() { + self.send_current_digest(stream).await?; + self.set_file_is_waiting(true); } + return Ok(None); } const BUF_SIZE: usize = 128 * 1024; - let mut buf: Vec = Vec::with_capacity(BUF_SIZE); - unsafe { - buf.set_len(BUF_SIZE); - } + let mut buf: Vec = vec![0; BUF_SIZE]; let mut compressed = false; let mut offset: usize = 0; loop { @@ -582,10 +574,7 @@ impl TransferJob { #[inline] pub fn job_completed(&self) -> bool { // has no error, Condition 2 - if !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) { - return true; - } - return false; + !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) } /// Get job error message, useful for getting status when job had finished @@ -660,7 +649,7 @@ pub fn new_dir(id: i32, path: String, files: Vec) -> Message { resp.set_dir(FileDirectory { id, path, - entries: files.into(), + entries: files, ..Default::default() }); let mut msg_out = Message::new(); @@ -692,7 +681,7 @@ pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec) action.set_receive(FileTransferReceiveRequest { id, path, - files: files.into(), + files, file_num, ..Default::default() }); @@ -736,8 +725,8 @@ pub fn remove_job(id: i32, jobs: &mut Vec) { } #[inline] -pub fn get_job(id: i32, jobs: &mut Vec) -> Option<&mut TransferJob> { - jobs.iter_mut().filter(|x| x.id() == id).next() +pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> { + jobs.iter_mut().find(|x| x.id() == id) } pub async fn handle_read_jobs( @@ -789,7 +778,7 @@ pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> { remove_all_empty_dir(&path.join(&entry.name)).ok(); } Ok(FileType::DirLink) | Ok(FileType::FileLink) => { - std::fs::remove_file(&path.join(&entry.name)).ok(); + std::fs::remove_file(path.join(&entry.name)).ok(); } _ => {} } @@ -813,7 +802,7 @@ pub fn create_dir(dir: &str) -> ResultType<()> { #[inline] pub fn transform_windows_path(entries: &mut Vec) { for entry in entries { - entry.name = entry.name.replace("\\", "/"); + entry.name = entry.name.replace('\\', "/"); } } diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index e57994f34..9e004376c 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -96,8 +96,24 @@ pub type ResultType = anyhow::Result; pub struct AddrMangle(); +#[inline] +pub fn try_into_v4(addr: SocketAddr) -> SocketAddr { + match addr { + SocketAddr::V6(v6) if !addr.ip().is_loopback() => { + if let Some(v4) = v6.ip().to_ipv4() { + SocketAddr::new(IpAddr::V4(v4), addr.port()) + } else { + addr + } + } + _ => addr, + } +} + impl AddrMangle { pub fn encode(addr: SocketAddr) -> Vec { + // not work with [:1]: + let addr = try_into_v4(addr); match addr { SocketAddr::V4(addr_v4) => { let tm = (SystemTime::now() @@ -129,22 +145,20 @@ impl AddrMangle { } pub fn decode(bytes: &[u8]) -> SocketAddr { + use std::convert::TryInto; + if bytes.len() > 16 { if bytes.len() != 18 { return Config::get_any_listen_addr(false); } - #[allow(invalid_value)] - let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; - tmp.copy_from_slice(&bytes[16..]); + let tmp: [u8; 2] = bytes[16..].try_into().unwrap(); let port = u16::from_le_bytes(tmp); - #[allow(invalid_value)] - let mut tmp: [u8; 16] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; - tmp.copy_from_slice(&bytes[..16]); + let tmp: [u8; 16] = bytes[..16].try_into().unwrap(); let ip = std::net::Ipv6Addr::from(tmp); return SocketAddr::new(IpAddr::V6(ip), port); } let mut padded = [0u8; 16]; - padded[..bytes.len()].copy_from_slice(&bytes); + padded[..bytes.len()].copy_from_slice(bytes); let number = u128::from_le_bytes(padded); let tm = (number >> 17) & (u32::max_value() as u128); let ip = (((number >> 49) - tm) as u32).to_le_bytes(); @@ -158,21 +172,9 @@ impl AddrMangle { pub fn get_version_from_url(url: &str) -> String { let n = url.chars().count(); - let a = url - .chars() - .rev() - .enumerate() - .filter(|(_, x)| x == &'-') - .next() - .map(|(i, _)| i); + let a = url.chars().rev().position(|x| x == '-'); if let Some(a) = a { - let b = url - .chars() - .rev() - .enumerate() - .filter(|(_, x)| x == &'.') - .next() - .map(|(i, _)| i); + let b = url.chars().rev().position(|x| x == '.'); if let Some(b) = b { if a > b { if url @@ -195,22 +197,30 @@ pub fn get_version_from_url(url: &str) -> String { } pub fn gen_version() { + if Ok("release".to_owned()) != std::env::var("PROFILE") { + return; + } + println!("cargo:rerun-if-changed=Cargo.toml"); use std::io::prelude::*; let mut file = File::create("./src/version.rs").unwrap(); - for line in read_lines("Cargo.toml").unwrap() { - if let Ok(line) = line { - let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect(); - if ab.len() == 2 && ab[0] == "version" { - file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) - .ok(); - break; - } + for line in read_lines("Cargo.toml").unwrap().flatten() { + let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect(); + if ab.len() == 2 && ab[0] == "version" { + file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) + .ok(); + break; } } // generate build date let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); - file.write_all(format!("pub const BUILD_DATE: &str = \"{}\";", build_date).as_bytes()) - .ok(); + file.write_all( + format!( + "#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{}\";", + build_date + ) + .as_bytes(), + ) + .ok(); file.sync_all().ok(); } @@ -230,20 +240,20 @@ pub fn is_valid_custom_id(id: &str) -> bool { pub fn get_version_number(v: &str) -> i64 { let mut n = 0; - for x in v.split(".") { + for x in v.split('.') { n = n * 1000 + x.parse::().unwrap_or(0); } n } pub fn get_modified_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(&path) + std::fs::metadata(path) .map(|m| m.modified().unwrap_or(UNIX_EPOCH)) .unwrap_or(UNIX_EPOCH) } pub fn get_created_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(&path) + std::fs::metadata(path) .map(|m| m.created().unwrap_or(UNIX_EPOCH)) .unwrap_or(UNIX_EPOCH) } @@ -276,32 +286,6 @@ pub fn get_time() -> i64 { .unwrap_or(0) as _ } -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_mangle() { - let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - - let addr = "[2001:db8::1]:8080".parse::().unwrap(); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - - let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - } - - #[test] - fn test_allow_err() { - allow_err!(Err("test err") as Result<(), &str>); - allow_err!( - Err("test err with msg") as Result<(), &str>, - "prompt {}", - "failed" - ); - } -} - #[inline] pub fn is_ipv4_str(id: &str) -> bool { regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") @@ -334,9 +318,31 @@ pub fn is_domain_port_str(id: &str) -> bool { } #[cfg(test)] -mod test_lib { +mod test { use super::*; + #[test] + fn test_mangle() { + let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8::1]:8080".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + } + + #[test] + fn test_allow_err() { + allow_err!(Err("test err") as Result<(), &str>); + allow_err!( + Err("test err with msg") as Result<(), &str>, + "prompt {}", + "failed" + ); + } + #[test] fn test_ipv6() { assert_eq!(is_ipv6_str("1:2:3"), true); @@ -373,4 +379,20 @@ mod test_lib { assert_eq!(is_domain_port_str("test.com:0"), true); assert_eq!(is_domain_port_str("test.com:98989"), true); } + + #[test] + fn test_mangle2() { + let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap(); + let addr_v4 = "127.0.0.1:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4); + assert_eq!( + AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())), + addr_v4 + ); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4); + let addr_v6 = "[ef::fe]:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); + let addr_v6 = "[::1]:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); + } } diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 602906990..0b66107fc 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -104,7 +104,7 @@ pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, if s.len() > VERSION_LEN { let version = &s[..VERSION_LEN]; if version == "00" { - if let Ok(v) = decrypt(&s[VERSION_LEN..].as_bytes()) { + if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) { return ( String::from_utf8_lossy(&v).to_string(), true, @@ -149,7 +149,7 @@ pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, boo } fn encrypt(v: &[u8]) -> Result { - if v.len() > 0 { + if !v.is_empty() { symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) } else { Err(()) @@ -157,7 +157,7 @@ fn encrypt(v: &[u8]) -> Result { } fn decrypt(v: &[u8]) -> Result, ()> { - if v.len() > 0 { + if !v.is_empty() { base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false)) } else { Err(()) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index e82416309..716025dc7 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -32,7 +32,7 @@ pub fn get_display_server() -> String { // loginctl has not given the expected output. try something else. if let Ok(sid) = std::env::var("XDG_SESSION_ID") { // could also execute "cat /proc/self/sessionid" - session = sid.to_owned(); + session = sid; } if session.is_empty() { session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default(); @@ -63,7 +63,7 @@ fn get_display_server_of_session(session: &str) -> String { if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty)) // And check if Xorg is running on that tty { - if xorg_results.trim_end().to_string() != "" { + if xorg_results.trim_end() != "" { // If it is, manually return "x11", otherwise return tty return "x11".to_owned(); } @@ -88,7 +88,7 @@ pub fn get_values_of_seat0(indices: Vec) -> Vec { if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains("seat0") { - if let Some(sid) = line.split_whitespace().nth(0) { + if let Some(sid) = line.split_whitespace().next() { if is_active(sid) { return indices .into_iter() @@ -103,7 +103,7 @@ pub fn get_values_of_seat0(indices: Vec) -> Vec { // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { - if let Some(sid) = line.split_whitespace().nth(0) { + if let Some(sid) = line.split_whitespace().next() { let d = get_display_server_of_session(sid); if is_active(sid) && d != "tty" { return indices diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 6f62163d1..a034b4e12 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -71,7 +71,7 @@ pub trait IsResolvedSocketAddr { impl IsResolvedSocketAddr for SocketAddr { fn resolve(&self) -> Option<&SocketAddr> { - Some(&self) + Some(self) } } @@ -120,12 +120,12 @@ pub async fn connect_tcp_local< if let Some(target) = target.resolve() { if let Some(local) = local { if local.is_ipv6() && target.is_ipv4() { - let target = query_nip_io(&target).await?; - return Ok(FramedStream::new(target, Some(local), ms_timeout).await?); + let target = query_nip_io(target).await?; + return FramedStream::new(target, Some(local), ms_timeout).await; } } } - Ok(FramedStream::new(target, local, ms_timeout).await?) + FramedStream::new(target, local, ms_timeout).await } #[inline] @@ -140,15 +140,14 @@ pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) .await? - .filter(|x| x.is_ipv6()) - .next() + .find(|x| x.is_ipv6()) .context("Failed to get ipv6 from nip.io") } #[inline] pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { if !ipv4 && crate::is_ipv4_str(&addr) { - if let Some(ip) = addr.split(":").next() { + if let Some(ip) = addr.split(':').next() { return addr.replace(ip, &format!("{}.nip.io", ip)); } } diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index a1322fc15..a7ac4eb3a 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -1,4 +1,5 @@ use crate::{bail, bytes_codec::BytesCodec, ResultType}; +use anyhow::Context as AnyhowCtx; use bytes::{BufMut, Bytes, BytesMut}; use futures::{SinkExt, StreamExt}; use protobuf::Message; @@ -209,7 +210,7 @@ impl FramedStream { if let Some(Ok(bytes)) = res.as_mut() { key.2 += 1; let nonce = Self::get_nonce(key.2); - match secretbox::open(&bytes, &nonce, &key.0) { + match secretbox::open(bytes, &nonce, &key.0) { Ok(res) => { bytes.clear(); bytes.put_slice(&res); @@ -245,16 +246,17 @@ impl FramedStream { const DEFAULT_BACKLOG: u32 = 128; -#[allow(clippy::never_loop)] pub async fn new_listener(addr: T, reuse: bool) -> ResultType { if !reuse { Ok(TcpListener::bind(addr).await?) } else { - for addr in lookup_host(&addr).await? { - let socket = new_socket(addr, true)?; - return Ok(socket.listen(DEFAULT_BACKLOG)?); - } - bail!("could not resolve to any address"); + let addr = lookup_host(&addr) + .await? + .next() + .context("could not resolve to any address")?; + new_socket(addr, true)? + .listen(DEFAULT_BACKLOG) + .map_err(anyhow::Error::msg) } } diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index 38121a4e1..bb0d071a2 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -1,11 +1,11 @@ -use crate::{bail, ResultType}; -use anyhow::anyhow; +use crate::ResultType; +use anyhow::{anyhow, Context}; use bytes::{Bytes, BytesMut}; use futures::{SinkExt, StreamExt}; use protobuf::Message; use socket2::{Domain, Socket, Type}; use std::net::SocketAddr; -use tokio::net::{ToSocketAddrs, UdpSocket}; +use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket}; use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs}; use tokio_util::{codec::BytesCodec, udp::UdpFramed}; @@ -37,39 +37,31 @@ fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result 0 { + socket.set_only_v6(false).ok(); + } socket.bind(&addr.into())?; Ok(socket) } impl FramedSocket { pub async fn new(addr: T) -> ResultType { - let socket = UdpSocket::bind(addr).await?; - Ok(Self::Direct(UdpFramed::new(socket, BytesCodec::new()))) + Self::new_reuse(addr, false, 0).await } - #[allow(clippy::never_loop)] - pub async fn new_reuse(addr: T) -> ResultType { - for addr in addr.to_socket_addrs()? { - let socket = new_socket(addr, true, 0)?.into_udp_socket(); - return Ok(Self::Direct(UdpFramed::new( - UdpSocket::from_std(socket)?, - BytesCodec::new(), - ))); - } - bail!("could not resolve to any address"); - } - - pub async fn new_with_buf_size( + pub async fn new_reuse( addr: T, + reuse: bool, buf_size: usize, ) -> ResultType { - for addr in addr.to_socket_addrs()? { - return Ok(Self::Direct(UdpFramed::new( - UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?, - BytesCodec::new(), - ))); - } - bail!("could not resolve to any address"); + let addr = lookup_host(&addr) + .await? + .next() + .context("could not resolve to any address")?; + Ok(Self::Direct(UdpFramed::new( + UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?, + BytesCodec::new(), + ))) } pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( @@ -104,11 +96,12 @@ impl FramedSocket { ) -> ResultType<()> { let addr = addr.into_target_addr()?.to_owned(); let send_data = Bytes::from(msg.write_to_bytes()?); - let _ = match self { - Self::Direct(f) => match addr { - TargetAddr::Ip(addr) => f.send((send_data, addr)).await?, - _ => {} - }, + match self { + Self::Direct(f) => { + if let TargetAddr::Ip(addr) = addr { + f.send((send_data, addr)).await? + } + } Self::ProxySocks(f) => f.send((send_data, addr)).await?, }; Ok(()) @@ -123,11 +116,12 @@ impl FramedSocket { ) -> ResultType<()> { let addr = addr.into_target_addr()?.to_owned(); - let _ = match self { - Self::Direct(f) => match addr { - TargetAddr::Ip(addr) => f.send((Bytes::from(msg), addr)).await?, - _ => {} - }, + match self { + Self::Direct(f) => { + if let TargetAddr::Ip(addr) = addr { + f.send((Bytes::from(msg), addr)).await? + } + } Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?, }; Ok(()) @@ -165,12 +159,12 @@ impl FramedSocket { } } - pub fn is_ipv4(&self) -> bool { + pub fn local_addr(&self) -> Option { if let FramedSocket::Direct(x) = self { if let Ok(v) = x.get_ref().local_addr() { - return v.is_ipv4(); + return Some(v); } } - true + None } } From ab026d5055ec248c7b215cfa610b266b874dad57 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 27 Jan 2023 13:03:35 +0800 Subject: [PATCH 235/734] fix unneccesary portable prompt window Signed-off-by: fufesou --- src/platform/windows.rs | 5 +++++ src/server/connection.rs | 2 +- src/ui_cm_interface.rs | 13 +++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 190834eb8..a77b92e07 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -839,6 +839,11 @@ pub fn check_update_broker_process() -> ResultType<()> { let cur_dir = exe_file.parent().unwrap(); let cur_exe = cur_dir.join(process_exe); + if !std::path::Path::new(&cur_exe).exists() { + std::fs::copy(origin_process_exe, cur_exe)?; + return Ok(()); + } + let ori_modified = fs::metadata(origin_process_exe)?.modified()?; if let Ok(metadata) = fs::metadata(&cur_exe) { if let Ok(cur_modified) = metadata.modified() { diff --git a/src/server/connection.rs b/src/server/connection.rs index cd5bd8cfa..c8814a3b1 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1274,7 +1274,7 @@ impl Connection { .retain(|_, v| v.0.elapsed() < SWITCH_SIDES_TIMEOUT); let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { - if let Some((instant, uuid_old)) = uuid_old { + if let Some((_instant, uuid_old)) = uuid_old { if uuid == uuid_old { self.from_switch = true; self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index ea3553c8a..d620bcbc9 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -449,14 +449,11 @@ pub async fn start_ipc(cm: ConnectionManager) { #[cfg(windows)] std::thread::spawn(move || { log::info!("try create privacy mode window"); - #[cfg(windows)] - { - if let Err(e) = crate::platform::windows::check_update_broker_process() { - log::warn!( - "Failed to check update broker process. Privacy mode may not work properly. {}", - e - ); - } + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); } allow_err!(crate::ui::win_privacy::start()); }); From 6f9b3ae466dbb77dfcc140b7313336118f84d53e Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 27 Jan 2023 21:11:46 +0800 Subject: [PATCH 236/734] Revert "opt: upgrade flutter ci/nightly to 3.7.0 stable" --- .github/workflows/flutter-ci.yml | 25 ++++++++------------ .github/workflows/flutter-nightly.yml | 24 +++++++------------ flutter/lib/utils/multi_window_manager.dart | 11 +++++---- flutter/macos/Runner/MainFlutterWindow.swift | 2 +- flutter/pubspec.yaml | 10 ++++---- 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index a2c67551d..4e98f311d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -13,7 +13,8 @@ on: env: LLVM_VERSION: "10.0" - FLUTTER_VERSION: "3.7.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -50,9 +51,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -852,12 +853,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.7.0 + # clone repo and reset to flutter 3.0.5 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.7.0 + # reset to flutter 3.0.5 git fetch - git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -882,17 +883,11 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - export PATH=/opt/flutter-elinux/bin:$PATH sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux. Run doctor to check if issues here. + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH flutter-elinux doctor -v - # Patch arm64 engine for flutter 3.6.0+ - flutter-elinux precache --linux - pushd /tmp - curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz - tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib - cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 - popd + # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 896afd005..b33a6dba0 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -8,7 +8,8 @@ on: env: LLVM_VERSION: "10.0" - FLUTTER_VERSION: "3.7.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility @@ -52,9 +53,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -998,12 +999,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.7.0 + # clone repo and reset to flutter 3.0.5 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.7.0 + # reset to flutter 3.0.5 git fetch - git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -1028,17 +1029,10 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - export PATH=/opt/flutter-elinux/bin:$PATH sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux. Run doctor to check if issues here. + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH flutter-elinux doctor -v - # Patch arm64 engine for flutter 3.6.0+ - flutter-elinux precache --linux - pushd /tmp - curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz - tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib - cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 - popd # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 7914a4c0a..ee19ac485 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -63,7 +63,8 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.RemoteDesktop)); + overrideType: WindowType.RemoteDesktop)) + ..show(); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -89,7 +90,8 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.FileTransfer)); + overrideType: WindowType.FileTransfer)) + ..show(); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -114,8 +116,9 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.PortForward)); + ..setTitle( + getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) + ..show(); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 108f5a5f8..540cd9ab9 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -7,7 +7,7 @@ import desktop_drop import device_info_plus_macos import flutter_custom_cursor import package_info_plus_macos -import path_provider_foundation +import path_provider_macos import screen_retriever import sqflite // import tray_manager diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0189ad9e4..a5535c8b7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 ffi: ^2.0.1 - path_provider: ^2.0.12 + path_provider: ^2.0.2 external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 @@ -75,14 +75,14 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^1.1.5 - flutter_improved_scrolling: + flutter_improved_scrolling: ^0.0.3 # 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 + # 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 From a529b14f2dc994d4e615c5333cb9f01d100ed1e1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 27 Jan 2023 23:11:03 +0800 Subject: [PATCH 237/734] peer card ActionMore from MouseReigon to InkWell to show hand pointer --- flutter/lib/common/widgets/peer_card.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 3feaef51d..c9af6328c 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1092,21 +1092,21 @@ Widget getOnline(double rightPadding, bool online) { } class ActionMore extends StatelessWidget { - final RxBool _iconMoreHover = false.obs; + final RxBool _hover = false.obs; @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => _iconMoreHover.value = true, - onExit: (_) => _iconMoreHover.value = false, + return InkWell( + onTap: () {}, + onHover: (value) => _hover.value = value, child: Obx(() => CircleAvatar( radius: 14, - backgroundColor: _iconMoreHover.value + backgroundColor: _hover.value ? Theme.of(context).scaffoldBackgroundColor : Theme.of(context).backgroundColor, child: Icon(Icons.more_vert, size: 18, - color: _iconMoreHover.value + color: _hover.value ? Theme.of(context).textTheme.titleLarge?.color : Theme.of(context) .textTheme From bf04a03cd120ecfa692dd94c6ade220f46cc5c40 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 27 Jan 2023 23:45:07 +0800 Subject: [PATCH 238/734] fix win, local detect some dead code Signed-off-by: fufesou --- Cargo.lock | 2 +- src/flutter_ffi.rs | 8 +++++++- src/keyboard.rs | 11 +++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 693ae7d4c..5c4af56e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4371,7 +4371,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#1be26c7e8ed0d43cebdd8331d467bb61130a2e6e" +source = "git+https://github.com/fufesou/rdev#238c9778da40056e2efda1e4264355bc89fb6358" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 992fff853..bcfafe9c2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -245,8 +245,13 @@ pub fn session_get_keyboard_mode(id: String) -> Option { } pub fn session_set_keyboard_mode(id: String, value: String) { + let mut mode_updated = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_keyboard_mode(value); + mode_updated = true; + } + if mode_updated { + crate::keyboard::update_grab_get_key_name(); } } @@ -1182,7 +1187,8 @@ pub fn main_update_me() -> SyncReturn { } pub fn set_cur_session_id(id: String) { - super::flutter::set_cur_session_id(id) + super::flutter::set_cur_session_id(id); + crate::keyboard::update_grab_get_key_name(); } pub fn install_show_run_without_install() -> SyncReturn { diff --git a/src/keyboard.rs b/src/keyboard.rs index de1abd231..054a39580 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -64,6 +64,8 @@ pub mod client { match state { GrabState::Ready => {} GrabState::Run => { + #[cfg(windows)] + update_grab_get_key_name(); #[cfg(any(target_os = "windows", target_os = "macos"))] KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); @@ -184,6 +186,15 @@ pub mod client { } } +#[cfg(windows)] +pub fn update_grab_get_key_name() { + match get_keyboard_mode_enum() { + KeyboardMode::Map => rdev::set_get_key_name(false), + KeyboardMode::Translate => rdev::set_get_key_name(true), + _ => {} + }; +} + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { From 7c865a80e943f7b82db25dcd61e2be689446ba9f Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 28 Jan 2023 09:36:36 +0800 Subject: [PATCH 239/734] fix build Signed-off-by: fufesou --- src/flutter_ffi.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index bcfafe9c2..c30c6c847 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -245,12 +245,13 @@ pub fn session_get_keyboard_mode(id: String) -> Option { } pub fn session_set_keyboard_mode(id: String, value: String) { - let mut mode_updated = false; + let mut _mode_updated = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_keyboard_mode(value); - mode_updated = true; + _mode_updated = true; } - if mode_updated { + #[cfg(windows)] + if _mode_updated { crate::keyboard::update_grab_get_key_name(); } } @@ -1188,6 +1189,7 @@ pub fn main_update_me() -> SyncReturn { pub fn set_cur_session_id(id: String) { super::flutter::set_cur_session_id(id); + #[cfg(windows)] crate::keyboard::update_grab_get_key_name(); } From a17f14b92abca34e01115dc6016909c5534735fd Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 28 Jan 2023 09:41:05 +0800 Subject: [PATCH 240/734] opt: upgrade flutter ci/nightly to 3.7.0 stable This reverts commit 6f9b3ae466dbb77dfcc140b7313336118f84d53e. --- .github/workflows/flutter-ci.yml | 25 ++++++++++++-------- .github/workflows/flutter-nightly.yml | 24 ++++++++++++------- flutter/lib/utils/multi_window_manager.dart | 11 ++++----- flutter/macos/Runner/MainFlutterWindow.swift | 2 +- flutter/pubspec.yaml | 10 ++++---- 5 files changed, 40 insertions(+), 32 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 4e98f311d..a2c67551d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -13,8 +13,7 @@ on: env: LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.7.0" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -51,9 +50,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -853,12 +852,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -883,11 +882,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v - # edit to corresponding arch + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b33a6dba0..896afd005 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -8,8 +8,7 @@ on: env: LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.7.0" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility @@ -53,9 +52,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -999,12 +998,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -1029,10 +1028,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index ee19ac485..7914a4c0a 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -63,8 +63,7 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.RemoteDesktop)) - ..show(); + overrideType: WindowType.RemoteDesktop)); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -90,8 +89,7 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.FileTransfer)) - ..show(); + overrideType: WindowType.FileTransfer)); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -116,9 +114,8 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle( - getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) - ..show(); + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.PortForward)); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 540cd9ab9..108f5a5f8 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -7,7 +7,7 @@ import desktop_drop import device_info_plus_macos import flutter_custom_cursor import package_info_plus_macos -import path_provider_macos +import path_provider_foundation import screen_retriever import sqflite // import tray_manager diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a5535c8b7..0189ad9e4 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 ffi: ^2.0.1 - path_provider: ^2.0.2 + path_provider: ^2.0.12 external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 @@ -75,14 +75,14 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^1.1.5 - flutter_improved_scrolling: ^0.0.3 + 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 + 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 From ef614d69c05ad213f239fc1811bbc4f8bd210d6c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 28 Jan 2023 09:33:57 +0800 Subject: [PATCH 241/734] fix: macos subwindow wont open --- flutter/lib/utils/multi_window_manager.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 7914a4c0a..224052bff 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/foundation.dart'; @@ -64,6 +65,9 @@ class RustDeskMultiWindowManager { ..center() ..setTitle(getWindowNameWithId(remoteId, overrideType: WindowType.RemoteDesktop)); + if (Platform.isMacOS) { + Future.microtask(() => remoteDesktopController.show()); + } registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -90,6 +94,9 @@ class RustDeskMultiWindowManager { ..center() ..setTitle(getWindowNameWithId(remoteId, overrideType: WindowType.FileTransfer)); + if (Platform.isMacOS) { + Future.microtask(() => fileTransferController.show()); + } registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -116,6 +123,9 @@ class RustDeskMultiWindowManager { ..center() ..setTitle(getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)); + if (Platform.isMacOS) { + Future.microtask(() => portForwardController.show()); + } registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { From 435e7749641369230adeaf79d708478c1756e599 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 10:39:13 +0800 Subject: [PATCH 242/734] fix switch sides delay #2893 Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 3 +-- src/server/connection.rs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 227002645..62289d5f0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -652,8 +652,7 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } - if (false && - pi.platform != kPeerPlatformAndroid && + if (pi.platform != kPeerPlatformAndroid && version_cmp(peer_version, '1.2.0') >= 0) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( diff --git a/src/server/connection.rs b/src/server/connection.rs index c8814a3b1..d3f7ac149 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1583,6 +1583,7 @@ impl Connection { uuid.to_string().as_ref(), ]) .ok(); + self.on_close_manually("switch sides", "peer"); return false; } } From d0d926bfb00caa189462c74f055588aff5c7c73b Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 12:02:14 +0800 Subject: [PATCH 243/734] opt connection close Signed-off-by: 21pages --- src/client.rs | 1 + 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/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 | 45 ++++++++++++++++++++++++++++++++++++---- 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/server/connection.rs | 35 +++++++++++++++++-------------- 34 files changed, 124 insertions(+), 50 deletions(-) diff --git a/src/client.rs b/src/client.rs index e9b8edf39..a6df6dbec 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2083,6 +2083,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"))) } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 72f55b44b..3c8df31b4 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 14e8a463d..537313e97 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "添加到地址簿"), ("Group", "小组"), ("Search", "搜索"), - ("Closed manually by the web console", "被web控制台手动关闭"), + ("Closed manually by web console", "被web控制台手动关闭"), ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), @@ -433,5 +433,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", "正常关闭"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e2935770c..d5a65cdbb 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 937990ea8..eda3b8a58 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a567877a2..774cac7e6 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Group", "Gruppe"), ("Search", "Suchen"), - ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), + ("Closed manually by web console", "Manuell über die Webkonsole beendet"), ("Local keyboard type", "Lokaler Tastaturtyp"), ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 839c69bbb..872cb30ec 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 2b109c18f..b7cb52804 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Añadir a la libreta de direcciones"), ("Group", "Grupo"), ("Search", "Búsqueda"), - ("Closed manually by the web console", "Cerrado manualmente por la consola web"), + ("Closed manually by web console", "Cerrado manualmente por la consola web"), ("Local keyboard type", "Tipo de teclado local"), ("Select local keyboard type", "Seleccionar tipo de teclado local"), ("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."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index da354579b..52ccf3786 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "افزودن به دفترچه آدرس"), ("Group", "گروه"), ("Search", "جستجو"), - ("Closed manually by the web console", "به صورت دستی توسط کنسول وب بسته شد"), + ("Closed manually by web console", "به صورت دستی توسط کنسول وب بسته شد"), ("Local keyboard type", "نوع صفحه کلید محلی"), ("Select local keyboard type", "نوع صفحه کلید محلی را انتخاب کنید"), ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9f1f23205..2feb026ff 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Ajouter au carnet d'adresses"), ("Group", "Groupe"), ("Search", "Rechercher"), - ("Closed manually by the web console", "Fermé manuellement par la console Web"), + ("Closed manually by web console", "Fermé manuellement par la console Web"), ("Local keyboard type", "Disposition du clavier local"), ("Select local keyboard type", "Selectionner la disposition du clavier local"), ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 6ec1152cd..8398fb72a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), ("Group", "Ομάδα"), ("Search", "Αναζήτηση"), - ("Closed manually by the web console", "Κλειστό χειροκίνητα από την κονσόλα web"), + ("Closed manually by web console", "Κλειστό χειροκίνητα από την κονσόλα web"), ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 295104a67..96eb63656 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 5604a0c52..b966b7af9 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", "Pencarian"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 7b979aff0..c144d7863 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Aggiungi alla rubrica"), ("Group", "Gruppo"), ("Search", "Cerca"), - ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), + ("Closed manually by web console", "Chiudi manualmente dalla console Web"), ("Local keyboard type", "Tipo di tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), ("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."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a280940c7..0466c48a9 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1cdf529ce..c0d0bec8a 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 59d26135f..fd8b520f4 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index ee4b45334..8853afe5a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj do Książki Adresowej"), ("Group", "Grypy"), ("Search", "Szukaj"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 66373a5e9..4a391c3fb 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5a137f391..b4a46c599 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index c5a9b529c..148723a5b 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Schimbă ID"), ("Website", "Site web"), ("About", "Despre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Fără sunet"), ("Audio Input", "Intrare audio"), ("Enhancements", "Îmbunătățiri"), @@ -116,7 +118,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Calitate bună a imaginii"), ("Balanced", "Calitate normală a imaginii"), ("Optimize reaction time", "Optimizează timpul de reacție"), - ("Custom", "Personalizare"), + ("Custom", "Personalizat"), ("Show remote cursor", "Afișează cursor la distanță"), ("Show quality monitor", "Afișează indicator de calitate"), ("Disable clipboard", "Dezactivează clipboard"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Se conectează mereu prin retransmisie"), ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), ("Login", "Conectare"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Deconectare"), ("Tags", "Etichetare"), ("Search ID", "Caută după ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Scală adaptivă"), ("General", "General"), ("Security", "Securitate"), - ("Account", "Cont"), ("Theme", "Temă"), ("Dark Theme", "Temă întunecată"), ("Dark", "Întunecat"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Acces direct IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Aplică"), ("Disconnect all devices?", "Vrei să deconectezi toate dispozitivele?"), ("Clear", "Golește"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Altele"), ("Confirm before closing multiple tabs", "Confirmă înainte de a închide mai multe file"), ("Keyboard Settings", "Configurare tastatură"), - ("Custom", "Personalizat"), ("Full Access", "Acces total"), ("Screen Share", "Partajare ecran"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), @@ -397,5 +401,38 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Solicită acces la dispozitivul tău"), ("Hide connection management window", "Ascunde fereastra de gestionare a conexiunilor"), ("hide_cm_tip", "Permite ascunderea ferestrei de gestionare doar dacă accepți începerea sesiunilor folosind parola permanentă"), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Closed as expected", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8103ae3a3..8e4411cb1 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Добавить в адресную книгу"), ("Group", "Группа"), ("Search", "Поиск"), - ("Closed manually by the web console", "Закрыто вручную через веб-консоль"), + ("Closed manually by web console", "Закрыто вручную через веб-консоль"), ("Local keyboard type", "Тип локальной клавиатуры"), ("Select local keyboard type", "Выберите тип локальной клавиатуры"), ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index c735cb28c..582cb58ae 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6a17cc906..cc35e3f3f 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj v adresar"), ("Group", "Skupina"), ("Search", "Iskanje"), - ("Closed manually by the web console", "Ročno zaprto iz spletne konzole"), + ("Closed manually by web console", "Ročno zaprto iz spletne konzole"), ("Local keyboard type", "Lokalna vrsta tipkovnice"), ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ebb43f6b7..3f11d72e1 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d9463318d..96ffa4d84 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj u adresar"), ("Group", "Grupa"), ("Search", "Pretraga"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 146e60f9a..2069826ee 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 729932973..26bc8ef51 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a78509e59..726bd8a9d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "เพิ่มไปยังสมุดรายชื่อ"), ("Group", "กลุ่ม"), ("Search", "ค้นหา"), - ("Closed manually by the web console", "ถูกปิดโดยเว็บคอนโซล"), + ("Closed manually by web console", "ถูกปิดโดยเว็บคอนโซล"), ("Local keyboard type", "ประเภทคีย์บอร์ด"), ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 483ee67e3..c7eb27205 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 459c517ff..e6d2dcb61 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "添加到地址簿"), ("Group", "小組"), ("Search", "搜索"), - ("Closed manually by the web console", "被web控制台手動關閉"), + ("Closed manually by web console", "被web控制台手動關閉"), ("Local keyboard type", "本地鍵盤類型"), ("Select local keyboard type", "請選擇本地鍵盤類型"), ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), @@ -433,5 +433,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", "正常關閉"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index ca99be12e..9276b184e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Додати IP до Адресної книги"), ("Group", "Група"), ("Search", "Пошук"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 53de4e67c..6649fbaa3 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index d3f7ac149..c259d54cf 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -101,7 +101,6 @@ pub struct Connection { lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, - close_manually: bool, #[allow(unused)] elevation_requested: bool, from_switch: bool, @@ -200,7 +199,6 @@ impl Connection { lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, - close_manually: false, elevation_requested: false, from_switch: false, }; @@ -271,7 +269,9 @@ impl Connection { } } ipc::Data::Close => { - conn.on_close_manually("connection manager", "peer").await; + conn.chat_unanswered = false; // seen + conn.send_close_reason_no_retry("").await; + conn.on_close("connection manager", true).await; break; } ipc::Data::ChatMessage{text} => { @@ -411,7 +411,8 @@ impl Connection { } Ok(conns) = hbbs_rx.recv() => { if conns.contains(&id) { - conn.on_close_manually("web console", "web console").await; + conn.send_close_reason_no_retry("Closed manually by web console").await; + conn.on_close("web console", true).await; break; } } @@ -441,7 +442,8 @@ impl Connection { Some(message::Union::Misc(m)) => { match &m.union { Some(misc::Union::StopService(_)) => { - conn.on_close_manually("stop service", "peer").await; + conn.send_close_reason_no_retry("").await; + conn.on_close("stop service", true).await; break; } _ => {}, @@ -540,6 +542,9 @@ impl Connection { "action": "close", })); ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().remove_connection(&conn.inner); + } log::info!("#{} connection loop exited", id); } @@ -1583,7 +1588,8 @@ impl Connection { uuid.to_string().as_ref(), ]) .ok(); - self.on_close_manually("switch sides", "peer"); + self.send_close_reason_no_retry("Closed as expected"); + self.on_close("switch sides", false); return false; } } @@ -1757,16 +1763,13 @@ impl Connection { } async fn on_close(&mut self, reason: &str, lock: bool) { - if let Some(s) = self.server.upgrade() { - s.write().unwrap().remove_connection(&self.inner); - } log::info!("#{} Connection closed: {}", self.inner.id(), reason); if lock && self.lock_after_session_end && self.keyboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered && !self.close_manually { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close @@ -1777,15 +1780,17 @@ impl Connection { self.port_forward_socket.take(); } - async fn on_close_manually(&mut self, close_from: &str, close_by: &str) { - self.close_manually = true; + // The `reason` should be consistent with `check_if_retry` if not empty + async fn send_close_reason_no_retry(&mut self, reason: &str) { let mut misc = Misc::new(); - misc.set_close_reason(format!("Closed manually by the {}", close_by)); + if reason.is_empty() { + misc.set_close_reason("Closed manually by the peer".to_string()); + } else { + misc.set_close_reason(reason.to_string()); + } let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; - self.on_close(&format!("Close requested from {}", close_from), false) - .await; SESSIONS.lock().unwrap().remove(&self.lr.my_id); } From 7c2d7df62e02d881f7ad32e34c3a94eb8e0bd132 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 16:39:24 +0800 Subject: [PATCH 244/734] quick support if right click && run as admin on win Signed-off-by: 21pages --- src/core_main.rs | 13 ++++++++----- src/server/portable_service.rs | 17 ++++++----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 8b99f6131..4a2f6164c 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -54,11 +54,6 @@ pub fn core_main() -> Option> { return core_main_invoke_new_connection(std::env::args()); } let click_setup = cfg!(windows) && args.is_empty() && crate::common::is_setup(&arg_exe); - #[cfg(not(feature = "flutter"))] - { - _is_quick_support = - cfg!(windows) && args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe"); - } if click_setup { args.push("--install".to_owned()); flutter_args.push("--install".to_string()); @@ -70,6 +65,14 @@ pub fn core_main() -> Option> { println!("{}", crate::VERSION); return None; } + #[cfg(windows)] + { + _is_quick_support |= !crate::platform::is_installed() + && args.is_empty() + && (arg_exe.to_lowercase().ends_with("qs.exe") + || (!click_setup && crate::platform::is_elevated(None).unwrap_or(false))); + crate::portable_service::client::set_quick_support(_is_quick_support); + } #[cfg(debug_assertions)] { use hbb_common::env_logger::*; diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 0651fd4ce..748cb39e4 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -459,6 +459,7 @@ pub mod client { static ref RUNNING: Arc> = Default::default(); static ref SHMEM: Arc>> = Default::default(); static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); + static ref QUICK_SUPPORT: Arc> = Default::default(); } pub enum StartPara { @@ -561,6 +562,10 @@ pub mod client { *SHMEM.lock().unwrap() = None; } + pub fn set_quick_support(v: bool) { + *QUICK_SUPPORT.lock().unwrap() = v; + } + fn set_dir_permission(dir: &PathBuf) -> bool { // // give Everyone RX permission std::process::Command::new("icacls") @@ -685,17 +690,7 @@ pub mod client { use DataPortableService::*; let rx = Arc::new(tokio::sync::Mutex::new(rx)); let postfix = IPC_SUFFIX; - #[cfg(feature = "flutter")] - let quick_support = { - let args: Vec<_> = std::env::args().collect(); - args.contains(&"--quick_support".to_string()) - }; - #[cfg(not(feature = "flutter"))] - let quick_support = std::env::current_exe() - .unwrap_or("".into()) - .to_string_lossy() - .to_lowercase() - .ends_with("qs.exe"); + let quick_support = QUICK_SUPPORT.lock().unwrap().clone(); match new_listener(postfix).await { Ok(mut incoming) => loop { From 3e4a8671152cdae2f33e926a3045b72e3edaf59e Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 16:41:47 +0800 Subject: [PATCH 245/734] opt elevation code Signed-off-by: 21pages --- flutter/lib/models/server_model.dart | 2 +- src/server/connection.rs | 165 +++++++++++++++------------ src/server/video_service.rs | 8 +- src/ui_cm_interface.rs | 4 +- 4 files changed, 102 insertions(+), 77 deletions(-) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 7703182cd..56dca4cdf 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -28,7 +28,7 @@ class ServerModel with ChangeNotifier { bool _inputOk = false; bool _audioOk = false; bool _fileOk = false; - bool _showElevation = true; + bool _showElevation = false; bool _hideCm = false; int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; diff --git a/src/server/connection.rs b/src/server/connection.rs index c259d54cf..c7aa7fe0c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3,6 +3,8 @@ use super::{input_service::*, *}; use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; +#[cfg(windows)] +use crate::portable_service::client as portable_client; use crate::video_service; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; @@ -101,8 +103,8 @@ pub struct Connection { lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, - #[allow(unused)] - elevation_requested: bool, + #[cfg(windows)] + portable: PortableState, from_switch: bool, } @@ -199,7 +201,8 @@ impl Connection { lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, - elevation_requested: false, + #[cfg(windows)] + portable: Default::default(), from_switch: false, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -247,14 +250,6 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] std::thread::spawn(move || Self::handle_input(rx_input, tx_cloned)); let mut second_timer = time::interval(Duration::from_secs(1)); - #[cfg(windows)] - let mut last_uac = false; - #[cfg(windows)] - let mut last_foreground_window_elevated = false; - #[cfg(windows)] - let mut last_portable_service_running = false; - #[cfg(windows)] - let is_installed = crate::platform::is_installed(); loop { tokio::select! { @@ -362,8 +357,7 @@ impl Connection { } #[cfg(windows)] ipc::Data::DataPortableService(ipc::DataPortableService::RequestStart) => { - use crate::portable_service::client; - if let Err(e) = client::start_portable_service(client::StartPara::Direct) { + if let Err(e) = portable_client::start_portable_service(portable_client::StartPara::Direct) { log::error!("Failed to start portable service from cm:{:?}", e); } } @@ -458,46 +452,7 @@ impl Connection { }, _ = second_timer.tick() => { #[cfg(windows)] - { - if !is_installed && conn.file_transfer.is_none() && conn.port_forward_socket.is_none(){ - let portable_service_running = crate::portable_service::client::running(); - if portable_service_running != last_portable_service_running { - last_portable_service_running = portable_service_running; - if portable_service_running && conn.elevation_requested { - let mut misc = Misc::new(); - misc.set_portable_service_running(portable_service_running); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); - if last_uac != uac { - last_uac = uac; - if !uac || !portable_service_running{ - let mut misc = Misc::new(); - misc.set_uac(uac); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap().clone(); - if last_foreground_window_elevated != foreground_window_elevated { - last_foreground_window_elevated = foreground_window_elevated; - if !foreground_window_elevated || !portable_service_running { - let mut misc = Misc::new(); - misc.set_foreground_window_elevated(foreground_window_elevated); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let show_elevation = !portable_service_running; - conn.send_to_cm(ipc::Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show_elevation))); - - } - } + conn.portable_check(); } _ = test_delay_timer.tick() => { if last_recv_time.elapsed() >= SEC30 { @@ -1537,15 +1492,14 @@ impl Connection { #[cfg(windows)] { let mut err = "No need to elevate".to_string(); - if !crate::platform::is_installed() - && !crate::portable_service::client::running() - { - use crate::portable_service::client; - err = client::start_portable_service(client::StartPara::Direct) - .err() - .map_or("".to_string(), |e| e.to_string()); + if !crate::platform::is_installed() && !portable_client::running() { + err = portable_client::start_portable_service( + portable_client::StartPara::Direct, + ) + .err() + .map_or("".to_string(), |e| e.to_string()); } - self.elevation_requested = err.is_empty(); + self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1557,18 +1511,14 @@ impl Connection { #[cfg(windows)] { let mut err = "No need to elevate".to_string(); - if !crate::platform::is_installed() - && !crate::portable_service::client::running() - { - use crate::portable_service::client; - err = client::start_portable_service(client::StartPara::Logon( - _r.username, - _r.password, - )) + if !crate::platform::is_installed() && !portable_client::running() { + err = portable_client::start_portable_service( + portable_client::StartPara::Logon(_r.username, _r.password), + ) .err() .map_or("".to_string(), |e| e.to_string()); } - self.elevation_requested = err.is_empty(); + self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1810,6 +1760,59 @@ impl Connection { pub fn alive_conns() -> Vec { ALIVE_CONNS.lock().unwrap().clone() } + + #[cfg(windows)] + fn portable_check(&mut self) { + if self.portable.is_installed + || self.file_transfer.is_some() + || self.port_forward_socket.is_some() + { + return; + } + let running = portable_client::running(); + let show_elevation = !running; + self.send_to_cm(ipc::Data::DataPortableService( + ipc::DataPortableService::CmShowElevation(show_elevation), + )); + if self.authorized { + let p = &mut self.portable; + if running != p.last_running { + p.last_running = running; + if running && p.elevation_requested { + let mut misc = Misc::new(); + misc.set_portable_service_running(running); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); + if p.last_uac != uac { + p.last_uac = uac; + if !uac || !running { + let mut misc = Misc::new(); + misc.set_uac(uac); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED + .lock() + .unwrap() + .clone(); + if p.last_foreground_window_elevated != foreground_window_elevated { + p.last_foreground_window_elevated = foreground_window_elevated; + if !foreground_window_elevated || !running { + let mut misc = Misc::new(); + misc.set_foreground_window_elevated(foreground_window_elevated); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + } + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -1984,3 +1987,25 @@ pub enum FileAuditType { RemoteSend = 0, RemoteReceive = 1, } + +#[cfg(windows)] +pub struct PortableState { + pub last_uac: bool, + pub last_foreground_window_elevated: bool, + pub last_running: bool, + pub is_installed: bool, + pub elevation_requested: bool, +} + +#[cfg(windows)] +impl Default for PortableState { + fn default() -> Self { + Self { + is_installed: crate::platform::is_installed(), + last_uac: Default::default(), + last_foreground_window_elevated: Default::default(), + last_running: Default::default(), + elevation_requested: Default::default(), + } + } +} diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 599dfbd54..d041a433c 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -956,15 +956,17 @@ fn start_uac_elevation_check() { START.call_once(|| { if !crate::platform::is_installed() && !crate::platform::is_root() - && !crate::platform::is_elevated(None).map_or(false, |b| b) + && !crate::portable_service::client::running() { std::thread::spawn(|| loop { std::thread::sleep(std::time::Duration::from_secs(1)); if let Ok(uac) = crate::ui::win_privacy::is_process_consent_running() { *IS_UAC_RUNNING.lock().unwrap() = uac; } - if let Ok(elevated) = crate::platform::is_foreground_window_elevated() { - *IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap() = elevated; + if !crate::platform::is_elevated(None).unwrap_or(false) { + if let Ok(elevated) = crate::platform::is_foreground_window_elevated() { + *IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap() = elevated; + } } }); } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index d620bcbc9..5d451e4d4 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -789,9 +789,7 @@ fn cm_inner_send(id: i32, data: Data) { pub fn can_elevate() -> bool { #[cfg(windows)] - { - return !crate::platform::is_installed() && !crate::portable_service::client::running(); - } + return !crate::platform::is_installed(); #[cfg(not(windows))] return false; } From 19f04f29c0c70d450237d26365fe178b97758d37 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 17:10:40 +0800 Subject: [PATCH 246/734] fix desktop dialog request focus Signed-off-by: 21pages --- flutter/lib/common.dart | 9 ++++----- flutter/lib/common/widgets/login.dart | 2 ++ flutter/lib/desktop/widgets/remote_menubar.dart | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index f4e0c2d75..6ee57ef50 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -608,12 +608,11 @@ class CustomAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { - FocusNode focusNode = FocusNode(); - // request focus if there is no focused FocusNode in the dialog - Future.delayed(Duration.zero, () { - if (!focusNode.hasFocus) focusNode.requestFocus(); - }); + // request focus FocusScopeNode scopeNode = FocusScopeNode(); + Future.delayed(Duration.zero, () { + if (!scopeNode.hasFocus) scopeNode.requestFocus(); + }); return FocusScope( node: scopeNode, autofocus: true, diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 2f10ac005..05fc1fc5c 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -666,6 +666,8 @@ Future verificationCodeDialog(UserPayload? user) async { child: const LinearProgressIndicator()), ], ), + onCancel: close, + onSubmit: onVerify, actions: [ dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Verify", onPressed: onVerify), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 62289d5f0..b9d793744 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -653,6 +653,7 @@ class _RemoteMenubarState extends State { )); } if (pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && // unsupport yet version_cmp(peer_version, '1.2.0') >= 0) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( From 8a88640b18f13dcb608ba0708a1cf599584f7221 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Sat, 28 Jan 2023 11:49:09 +0100 Subject: [PATCH 247/734] Update it.rs --- src/lang/it.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index d7340b27f..322c324ce 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -41,9 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Data della build"), + ("Version", "Versione"), + ("Home", "Home"), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), @@ -436,6 +436,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", ""), + ("Closed as expected", "Chiuso come previsto"), ].iter().cloned().collect(); } From 733a43df07d6710f24999df6eb376aeea2736532 Mon Sep 17 00:00:00 2001 From: csf Date: Sat, 28 Jan 2023 21:24:49 +0900 Subject: [PATCH 248/734] 1. fix get_api_server. 2. add device info in LoginRequest --- flutter/lib/common/hbbs/hbbs.dart | 32 ++++++++++++++++--------------- src/common.rs | 13 +++++++++++-- src/flutter_ffi.rs | 4 ++++ 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index 27238db67..4717143fd 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -1,5 +1,9 @@ +import 'dart:io'; + import 'package:flutter_hbb/models/peer_model.dart'; +import '../../models/platform_model.dart'; + class HttpType { static const kAuthReqTypeAccount = "account"; static const kAuthReqTypeMobile = "mobile"; @@ -48,6 +52,16 @@ class PeerPayload { } } +class DeviceInfo { + static Map toJson() { + final Map data = {}; + data['os'] = Platform.operatingSystem; + data['type'] = "client"; + data['name'] = bind.mainGetHostname(); + return data; + } +} + class LoginRequest { String? username; String? password; @@ -56,7 +70,7 @@ class LoginRequest { bool? autoLogin; String? type; String? verificationCode; - String? deviceInfo; + Map deviceInfo = DeviceInfo.toJson(); LoginRequest( {this.username, @@ -65,19 +79,7 @@ class LoginRequest { this.uuid, this.autoLogin, this.type, - this.verificationCode, - this.deviceInfo}); - - LoginRequest.fromJson(Map json) { - username = json['username']; - password = json['password']; - id = json['id']; - uuid = json['uuid']; - autoLogin = json['autoLogin']; - type = json['type']; - verificationCode = json['verificationCode']; - deviceInfo = json['deviceInfo']; - } + this.verificationCode}); Map toJson() { final Map data = {}; @@ -88,7 +90,7 @@ class LoginRequest { data['autoLogin'] = autoLogin ?? ''; data['type'] = type ?? ''; data['verificationCode'] = verificationCode ?? ''; - data['deviceInfo'] = deviceInfo ?? ''; + data['deviceInfo'] = deviceInfo; return data; } } diff --git a/src/common.rs b/src/common.rs index cdf57ae3d..2bf287feb 100644 --- a/src/common.rs +++ b/src/common.rs @@ -451,6 +451,7 @@ pub fn run_me>(args: Vec) -> std::io::Result String { // fix bug of whoami #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -459,6 +460,14 @@ pub fn username() -> String { return DEVICE_NAME.lock().unwrap().clone(); } +#[inline] +pub fn hostname() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return whoami::hostname(); + #[cfg(any(target_os = "android", target_os = "ios"))] + return DEVICE_NAME.lock().unwrap().clone(); +} + #[inline] pub fn check_port(host: T, port: i32) -> String { hbb_common::socket_client::check_port(host, port) @@ -581,9 +590,9 @@ pub fn get_api_server(api: String, custom: String) -> String { if !s0.is_empty() { let s = crate::increase_port(&s0, -2); if s == s0 { - format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); + return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); } else { - format!("http://{}", s); + return format!("http://{}", s); } } "https://admin.rustdesk.com".to_owned() diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c30c6c847..ebaa160f1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -523,6 +523,10 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } +pub fn main_get_hostname() -> SyncReturn { + SyncReturn(crate::common::hostname()) +} + pub fn main_change_id(new_id: String) { change_id(new_id) } From 813b9ea79def023dc39982ba16befbf89c2a2f08 Mon Sep 17 00:00:00 2001 From: csf Date: Sat, 28 Jan 2023 22:02:42 +0900 Subject: [PATCH 249/734] fix logout failed --- flutter/lib/models/user_model.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index b0eebee53..6694d8c5c 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -80,13 +80,15 @@ class UserModel { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); try { final url = await bind.mainGetApiServer(); + final authHeaders = getHttpHeaders(); + authHeaders['Content-Type'] = "application/json"; await http .post(Uri.parse('$url/api/logout'), - body: { + body: jsonEncode({ 'id': await bind.mainGetMyId(), 'uuid': await bind.mainGetUuid(), - }, - headers: getHttpHeaders()) + }), + headers: authHeaders) .timeout(Duration(seconds: 2)); } catch (e) { print("request /api/logout failed: err=$e"); From d04f047d14a543b36fe372481b924922348898ad Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 28 Jan 2023 21:11:03 +0800 Subject: [PATCH 250/734] feat mouse click and move through monitor widget Signed-off-by: fufesou --- flutter/lib/common/widgets/overlay.dart | 8 ++- flutter/lib/desktop/pages/remote_page.dart | 60 ++++++++++++++-------- flutter/lib/mobile/pages/remote_page.dart | 6 ++- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 81797962e..d9684bace 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -324,10 +324,8 @@ class QualityMonitor extends StatelessWidget { Widget build(BuildContext context) => ChangeNotifierProvider.value( value: qualityMonitorModel, child: Consumer( - builder: (context, qualityMonitorModel, child) => Positioned( - top: 10, - right: 10, - child: qualityMonitorModel.show + builder: (context, qualityMonitorModel, child) => + qualityMonitorModel.show ? Container( padding: const EdgeInsets.all(8), color: MyTheme.canvasColor.withAlpha(120), @@ -357,5 +355,5 @@ class QualityMonitor extends StatelessWidget { ], ), ) - : const SizedBox.shrink()))); + : const SizedBox.shrink())); } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index fb67154bc..2e4668159 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -279,6 +279,34 @@ class _RemotePageState extends State } } + Widget _buildRawPointerMouseRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawPointerMouseRegion( + onEnter: enterView, + onExit: leaveView, + onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, + inputModel: _ffi.inputModel, + child: child, + ); + } + Widget getBodyForDesktop(BuildContext context) { var paints = [ MouseRegion(onEnter: (evt) { @@ -295,27 +323,8 @@ class _RemotePageState extends State cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, - listenerBuilder: (child) => RawPointerMouseRegion( - onEnter: enterView, - onExit: leaveView, - onPointerDown: (event) { - // A double check for blur status. - // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. - // Sometimes the system does not send the necessary focus event to flutter. We should manually - // handle this inconsistent status by setting `_isWindowBlur` to false. So we can - // ensure the grab-key thread is running when our users are clicking the remote canvas. - if (_isWindowBlur) { - debugPrint( - "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); - _isWindowBlur = false; - } - if (!_rawKeyFocusNode.hasFocus) { - _rawKeyFocusNode.requestFocus(); - } - }, - inputModel: _ffi.inputModel, - child: child, - ), + listenerBuilder: (child) => + _buildRawPointerMouseRegion(child, enterView, leaveView), ); })) ]; @@ -328,7 +337,14 @@ class _RemotePageState extends State zoomCursor: _zoomCursor, )))); } - paints.add(QualityMonitor(_ffi.qualityMonitorModel)); + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawPointerMouseRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); paints.add(RemoteMenubar( id: widget.id, ffi: _ffi, diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 0a10d8011..c4b07b375 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -497,7 +497,11 @@ class _RemotePageState extends State { child: Stack(children: () { final paints = [ ImagePaint(), - QualityMonitor(gFFI.qualityMonitorModel), + Positioned( + top: 10, + right: 10, + child: QualityMonitor(gFFI.qualityMonitorModel), + ), getHelpTools(), SizedBox( width: 0, From eb831a912a8250e2f735bbffb419378c71527319 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 28 Jan 2023 21:52:19 +0100 Subject: [PATCH 251/734] Update de.rs --- src/lang/de.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 15e98e529..11ce96f6b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -42,9 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Erstelldatum"), + ("Version", "Version"), + ("Home", "Startseite"), ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), ("Hardware Codec", "Hardware-Codec"), @@ -244,7 +244,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote ID", "Entfernte ID"), ("Paste", "Einfügen"), ("Paste here?", "Hier einfügen?"), - ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich trennen?"), + ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich schließen?"), ("Download new version", "Neue Version herunterladen"), ("Touch mode", "Touch-Modus"), ("Mouse mode", "Mausmodus"), @@ -267,8 +267,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Hinweis"), ("Connection", "Verbindung"), ("Share Screen", "Bildschirm freigeben"), - ("CLOSE", "DEAKTIV."), - ("OPEN", "AKTIVIER."), + ("CLOSE", "SCHLIEẞEN"), + ("OPEN", "ÖFFNEN"), ("Chat", "Chat"), ("Total", "Gesamt"), ("items", "Einträge"), @@ -387,7 +387,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 Bildschirm aus, der freigegeben werden soll (auf der Peer-Seite arbeiten)."), + ("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)."), ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), ("or", "oder"), @@ -410,7 +410,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Group", "Gruppe"), ("Search", "Suchen"), - ("Closed manually by web console", "Manuell über die Webkonsole beendet"), + ("Closed manually by web console", "Manuell über die Webkonsole geschlossen"), ("Local keyboard type", "Lokaler Tastaturtyp"), ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), @@ -436,6 +436,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", ""), + ("Closed as expected", "Wie erwartet geschlossen"), ].iter().cloned().collect(); } From 7e0c9e17df28710387249e7daf34fee107ce8f7a Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 28 Jan 2023 20:32:46 +0800 Subject: [PATCH 252/734] set cursor mode according to availible modes Signed-off-by: fufesou --- libs/scrap/src/wayland/pipewire.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index c1c84f98e..d1a8d9f85 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -473,7 +473,17 @@ fn request_screen_cast( args.insert("multiple".into(), Variant(Box::new(true))); args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); - let cursor_mode = if capture_cursor { 2u32 } else { 1u32 }; + let mut cursor_mode = 0u32; + let mut available_cursor_modes = 0u32; + if let Ok(modes) = portal.available_cursor_modes() { + available_cursor_modes = modes; + } + if capture_cursor { + cursor_mode = 2u32 & available_cursor_modes; + } + if cursor_mode == 0 { + cursor_mode = 1u32 & available_cursor_modes; + } let plasma = std::env::var("DESKTOP_SESSION").map_or(false, |s| s.contains("plasma")); if plasma && capture_cursor { // Warn the user if capturing the cursor is tried on kde as this can crash @@ -483,7 +493,9 @@ fn request_screen_cast( desktop, see https://bugs.kde.org/show_bug.cgi?id=435042 for details! \ You have been warned."); } - args.insert("cursor_mode".into(), Variant(Box::new(cursor_mode))); + if cursor_mode > 0 { + args.insert("cursor_mode".into(), Variant(Box::new(cursor_mode))); + } let session: dbus::Path = r .results .get("session_handle") From b84f3ba1ee7708d8150e53c6a674bbcf1ffc1bc4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 28 Jan 2023 22:19:28 +0800 Subject: [PATCH 253/734] init wayland to update var 'cursor embeded' Signed-off-by: fufesou --- libs/scrap/src/common/mod.rs | 4 ++-- libs/scrap/src/common/wayland.rs | 18 ++++++++++++++++-- libs/scrap/src/wayland/pipewire.rs | 6 ++++++ src/common.rs | 2 +- src/server/wayland.rs | 5 +++-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 1de2f89d6..1df96f511 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -12,7 +12,7 @@ cfg_if! { mod x11; pub use self::linux::*; pub use self::x11::Frame; - pub use self::wayland::set_map_err; + pub use self::wayland::{set_map_err, detect_cursor_embeded}; } else { mod x11; pub use self::x11::*; @@ -76,7 +76,7 @@ pub fn is_cursor_embedded() -> bool { if is_x11() { x11::IS_CURSOR_EMBEDDED } else { - wayland::IS_CURSOR_EMBEDDED + unsafe { wayland::IS_CURSOR_EMBEDDED } } } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index e625fca7e..c807479f1 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,12 +4,26 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -pub const IS_CURSOR_EMBEDDED: bool = true; +pub static mut IS_CURSOR_EMBEDDED: bool = true; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); } +pub fn detect_cursor_embeded() { + if unsafe { IS_CURSOR_EMBEDDED } { + use crate::common::wayland::pipewire::get_available_cursor_modes; + match get_available_cursor_modes() { + Ok(modes) => unsafe { + IS_CURSOR_EMBEDDED = (modes & 0x02) > 0; + }, + Err(..) => unsafe { + IS_CURSOR_EMBEDDED = false; + }, + } + } +} + pub fn set_map_err(f: fn(err: String) -> io::Error) { *MAP_ERR.write().unwrap() = Some(f); } @@ -74,7 +88,7 @@ impl Display { } pub fn all() -> io::Result> { - Ok(pipewire::get_capturables(true) + Ok(pipewire::get_capturables(unsafe { IS_CURSOR_EMBEDDED }) .map_err(map_err)? .drain(..) .map(|x| Display(x)) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index d1a8d9f85..fefab9b77 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -415,6 +415,12 @@ static mut INIT: bool = false; const RESTORE_TOKEN: &str = "restore_token"; const RESTORE_TOKEN_CONF_KEY: &str = "wayland-restore-token"; +pub fn get_available_cursor_modes() -> Result { + let conn = SyncConnection::new_session()?; + let portal = get_portal(&conn); + portal.available_cursor_modes() +} + // mostly inspired by https://gitlab.gnome.org/snippets/19 fn request_screen_cast( capture_cursor: bool, diff --git a/src/common.rs b/src/common.rs index 2bf287feb..c2d5a81f0 100644 --- a/src/common.rs +++ b/src/common.rs @@ -52,7 +52,7 @@ pub fn global_init() -> bool { #[cfg(target_os = "linux")] { if !*IS_X11 { - crate::server::wayland::set_wayland_scrap_map_err(); + crate::server::wayland::init(); } } true diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 68b9c37cf..96fc2fff6 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,6 +1,6 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{set_map_err, Capturer, Display, Frame, TraitCapturer}; +use scrap::{detect_cursor_embeded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use super::video_service::{ @@ -12,7 +12,8 @@ lazy_static::lazy_static! { static ref LOG_SCRAP_COUNT: Mutex = Mutex::new(0); } -pub fn set_wayland_scrap_map_err() { +pub fn init() { + detect_cursor_embeded(); set_map_err(map_err_scrap); } From c0adc142159bea13e72db9b986a2f54b7ebeb9a5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 29 Jan 2023 09:26:55 +0800 Subject: [PATCH 254/734] misspelling Signed-off-by: fufesou --- libs/scrap/src/common/mod.rs | 2 +- libs/scrap/src/common/wayland.rs | 2 +- src/server/wayland.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 1df96f511..af1bc4d5e 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -12,7 +12,7 @@ cfg_if! { mod x11; pub use self::linux::*; pub use self::x11::Frame; - pub use self::wayland::{set_map_err, detect_cursor_embeded}; + pub use self::wayland::{set_map_err, detect_cursor_embedded}; } else { mod x11; pub use self::x11::*; diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index c807479f1..9d62b87d2 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -10,7 +10,7 @@ lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); } -pub fn detect_cursor_embeded() { +pub fn detect_cursor_embedded() { if unsafe { IS_CURSOR_EMBEDDED } { use crate::common::wayland::pipewire::get_available_cursor_modes; match get_available_cursor_modes() { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 96fc2fff6..eada6971a 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,6 +1,6 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{detect_cursor_embeded, set_map_err, Capturer, Display, Frame, TraitCapturer}; +use scrap::{detect_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use super::video_service::{ @@ -13,7 +13,7 @@ lazy_static::lazy_static! { } pub fn init() { - detect_cursor_embeded(); + detect_cursor_embedded(); set_map_err(map_err_scrap); } From 340897ab1805a5ccc2d9b2c7c8af224643e4017f Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 29 Jan 2023 09:57:05 +0800 Subject: [PATCH 255/734] set cursor embedded Signed-off-by: fufesou --- src/server/wayland.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server/wayland.rs b/src/server/wayland.rs index eada6971a..817b8adb7 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,6 +1,9 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{detect_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; +use scrap::{ + detect_cursor_embedded, is_cursor_embedded, set_map_err, Capturer, Display, Frame, + TraitCapturer, +}; use std::io; use super::video_service::{ @@ -130,7 +133,7 @@ pub(super) async fn check_init() -> ResultType<()> { let num = all.len(); let (primary, mut displays) = super::video_service::get_displays_2(&all); for display in displays.iter_mut() { - display.cursor_embedded = true; + display.cursor_embedded = is_cursor_embedded(); } let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); From d1090fc62c7d62d0bcf26d45b4675360118ba756 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 29 Jan 2023 10:58:04 +0800 Subject: [PATCH 256/734] ensure init cursor embedded Signed-off-by: fufesou --- libs/scrap/src/common/mod.rs | 4 ++-- libs/scrap/src/common/wayland.rs | 31 +++++++++++++++++++------------ src/server/wayland.rs | 6 +----- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index af1bc4d5e..45aafe7c5 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -12,7 +12,7 @@ cfg_if! { mod x11; pub use self::linux::*; pub use self::x11::Frame; - pub use self::wayland::{set_map_err, detect_cursor_embedded}; + pub use self::wayland::set_map_err; } else { mod x11; pub use self::x11::*; @@ -76,7 +76,7 @@ pub fn is_cursor_embedded() -> bool { if is_x11() { x11::IS_CURSOR_EMBEDDED } else { - unsafe { wayland::IS_CURSOR_EMBEDDED } + wayland::is_cursor_embedded() } } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 9d62b87d2..86afd5d82 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,22 +4,29 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -pub static mut IS_CURSOR_EMBEDDED: bool = true; +static mut IS_CURSOR_EMBEDDED: Option = None; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); } -pub fn detect_cursor_embedded() { - if unsafe { IS_CURSOR_EMBEDDED } { - use crate::common::wayland::pipewire::get_available_cursor_modes; - match get_available_cursor_modes() { - Ok(modes) => unsafe { - IS_CURSOR_EMBEDDED = (modes & 0x02) > 0; - }, - Err(..) => unsafe { - IS_CURSOR_EMBEDDED = false; - }, +pub fn is_cursor_embedded() -> bool { + unsafe { + if IS_CURSOR_EMBEDDED.is_none() { + init_cursor_embedded(); + } + IS_CURSOR_EMBEDDED.unwrap_or(false) + } +} + +unsafe fn init_cursor_embedded() { + use crate::common::wayland::pipewire::get_available_cursor_modes; + match get_available_cursor_modes() { + Ok(modes) => { + IS_CURSOR_EMBEDDED = Some((modes & 0x02) > 0); + } + Err(..) => { + IS_CURSOR_EMBEDDED = Some(false); } } } @@ -88,7 +95,7 @@ impl Display { } pub fn all() -> io::Result> { - Ok(pipewire::get_capturables(unsafe { IS_CURSOR_EMBEDDED }) + Ok(pipewire::get_capturables(is_cursor_embedded()) .map_err(map_err)? .drain(..) .map(|x| Display(x)) diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 817b8adb7..954f1ed1d 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,9 +1,6 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{ - detect_cursor_embedded, is_cursor_embedded, set_map_err, Capturer, Display, Frame, - TraitCapturer, -}; +use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use super::video_service::{ @@ -16,7 +13,6 @@ lazy_static::lazy_static! { } pub fn init() { - detect_cursor_embedded(); set_map_err(map_err_scrap); } From 176847c51eda697839468b23f1ac679c2b73e618 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 29 Jan 2023 14:27:57 +0800 Subject: [PATCH 257/734] fix warning Signed-off-by: 21pages --- libs/scrap/src/dxgi/mod.rs | 7 +++++++ src/platform/windows.rs | 3 +++ src/server/connection.rs | 7 +++---- src/ui_session_interface.rs | 1 - 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 152f502a3..4a0a53402 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -58,6 +58,7 @@ impl Capturer { let mut device = ptr::null_mut(); let mut context = ptr::null_mut(); let mut duplication = ptr::null_mut(); + #[allow(invalid_value)] let mut desc = unsafe { mem::MaybeUninit::uninit().assume_init() }; let mut gdi_capturer = None; @@ -176,6 +177,7 @@ impl Capturer { unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> { let mut frame = ptr::null_mut(); + #[allow(invalid_value)] let mut info = mem::MaybeUninit::uninit().assume_init(); wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?; @@ -185,6 +187,7 @@ impl Capturer { return Err(std::io::ErrorKind::WouldBlock.into()); } + #[allow(invalid_value)] let mut rect = mem::MaybeUninit::uninit().assume_init(); if self.fastlane { wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?; @@ -204,6 +207,7 @@ impl Capturer { ); let texture = ComPtr(texture); + #[allow(invalid_value)] let mut texture_desc = mem::MaybeUninit::uninit().assume_init(); (*texture.0).GetDesc(&mut texture_desc); @@ -362,6 +366,7 @@ impl Displays { let mut all = Vec::new(); let mut i: DWORD = 0; loop { + #[allow(invalid_value)] let mut d: DISPLAY_DEVICEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; d.cb = std::mem::size_of::() as _; let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), i, &mut d as _, 0) }; @@ -382,6 +387,7 @@ impl Displays { gdi: true, }; disp.desc.DeviceName = d.DeviceName; + #[allow(invalid_value)] let mut m: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; m.dmSize = std::mem::size_of::() as _; m.dmDriverExtra = 0; @@ -441,6 +447,7 @@ impl Displays { // We get the display's details. let desc = unsafe { + #[allow(invalid_value)] let mut desc = mem::MaybeUninit::uninit().assume_init(); (*output.0).GetDesc(&mut desc); desc diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a77b92e07..b778283a5 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -49,6 +49,7 @@ use winreg::RegKey; pub fn get_cursor_pos() -> Option<(i32, i32)> { unsafe { + #[allow(invalid_value)] let mut out = mem::MaybeUninit::uninit().assume_init(); if GetCursorPos(&mut out) == FALSE { return None; @@ -61,6 +62,7 @@ pub fn reset_input_cache() {} pub fn get_cursor() -> ResultType> { unsafe { + #[allow(invalid_value)] let mut ci: CURSORINFO = mem::MaybeUninit::uninit().assume_init(); ci.cbSize = std::mem::size_of::() as _; if crate::portable_service::client::get_cursor_info(&mut ci) == FALSE { @@ -79,6 +81,7 @@ struct IconInfo(ICONINFO); impl IconInfo { fn new(icon: HICON) -> ResultType { unsafe { + #[allow(invalid_value)] let mut ii = mem::MaybeUninit::uninit().assume_init(); if GetIconInfo(icon, &mut ii) == FALSE { Err(io::Error::last_os_error().into()) diff --git a/src/server/connection.rs b/src/server/connection.rs index c7aa7fe0c..e4b667d54 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -138,7 +138,6 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); -const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(10); impl Connection { pub async fn start( @@ -1231,7 +1230,7 @@ impl Connection { SWITCH_SIDES_UUID .lock() .unwrap() - .retain(|_, v| v.0.elapsed() < SWITCH_SIDES_TIMEOUT); + .retain(|_, v| v.0.elapsed() < Duration::from_secs(10)); let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { if let Some((_instant, uuid_old)) = uuid_old { @@ -1538,8 +1537,8 @@ impl Connection { uuid.to_string().as_ref(), ]) .ok(); - self.send_close_reason_no_retry("Closed as expected"); - self.on_close("switch sides", false); + self.send_close_reason_no_retry("Closed as expected").await; + self.on_close("switch sides", false).await; return false; } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 48f6c1090..4fc5db743 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -6,7 +6,6 @@ use crate::client::{ }; use crate::common::{self, GrabState}; use crate::keyboard; -use crate::ui_interface::using_public_server; use crate::{client::Data, client::Interface}; use async_trait::async_trait; use bytes::Bytes; From d1070b88bb3dbdaee6dac4a7f3950e027e9595fd Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 29 Jan 2023 14:36:20 +0800 Subject: [PATCH 258/734] dismiss menu after switching monitor Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index b9d793744..07944649c 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -362,6 +362,9 @@ class _RemoteMenubarState extends State { ), )), onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } RxInt display = CurrentDisplayState.find(widget.id); if (display.value != i) { bind.sessionSwitchDisplay(id: widget.id, value: i); From fc15209d08b980a5e367e2bb8c530a0f91c94aeb Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Sun, 29 Jan 2023 14:02:06 +0330 Subject: [PATCH 259/734] Update fa.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Closed as expected"-> "طبق انتظار بسته شد" --- src/lang/fa.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 3b1bcfaf5..15ef1b843 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -436,6 +436,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", ""), + ("Closed as expected", "طبق انتظار بسته شد"), ].iter().cloned().collect(); } From 92748f7ef4d49112f1512b57879bb735034e870d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 29 Jan 2023 23:30:49 +0800 Subject: [PATCH 260/734] adjust tab colors to fix issue #2957 --- .../lib/desktop/widgets/tabbar_widget.dart | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index ddc0e7729..598b2cc4c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -765,7 +765,7 @@ class _ListView extends StatelessWidget { tabBuilder: tabBuilder, tabMenuBuilder: tabMenuBuilder, maxLabelWidth: maxLabelWidth, - selectedTabBackgroundColor: selectedTabBackgroundColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? MyTheme.tabbar(context).selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); @@ -910,7 +910,7 @@ class _TabState extends State<_Tab> with RestorationMixin { tabSelected: isSelected, onClose: () => widget.onClose(), ))) - ])).paddingSymmetric(horizontal: 10), + ])).paddingOnly(left: 10, right: 5), Offstage( offstage: !showDivider, child: VerticalDivider( @@ -956,6 +956,7 @@ class _CloseButton extends StatelessWidget { child: Offstage( offstage: !visible, child: InkWell( + hoverColor: MyTheme.tabbar(context).closeHoverColor, customBorder: const RoundedRectangleBorder(), onTap: () => onClose(), child: Icon( @@ -966,7 +967,7 @@ class _CloseButton extends StatelessWidget { : MyTheme.tabbar(context).unSelectedIconColor, ), ), - )).paddingOnly(left: 5); + )).paddingOnly(left: 10); } } @@ -1055,6 +1056,8 @@ class TabbarTheme extends ThemeExtension { final Color? unSelectedIconColor; final Color? dividerColor; final Color? hoverColor; + final Color? closeHoverColor; + final Color? selectedTabBackgroundColor; const TabbarTheme( {required this.selectedTabIconColor, @@ -1064,27 +1067,33 @@ class TabbarTheme extends ThemeExtension { required this.selectedIconColor, required this.unSelectedIconColor, required this.dividerColor, - required this.hoverColor}); + required this.hoverColor, + required this.closeHoverColor, + required this.selectedTabBackgroundColor}); static const light = TabbarTheme( selectedTabIconColor: MyTheme.accent, unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241), - selectedTextColor: Color.fromARGB(255, 26, 26, 26), - unSelectedTextColor: Color.fromARGB(255, 96, 96, 96), + selectedTextColor: Colors.black, + unSelectedTextColor: Color.fromARGB(255, 112, 112, 112), selectedIconColor: Color.fromARGB(255, 26, 26, 26), unSelectedIconColor: Color.fromARGB(255, 96, 96, 96), dividerColor: Color.fromARGB(255, 238, 238, 238), - hoverColor: Color.fromARGB(51, 158, 158, 158)); + hoverColor: Color.fromARGB(51, 158, 158, 158), + closeHoverColor: Colors.black, + selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240)); static const dark = TabbarTheme( selectedTabIconColor: MyTheme.accent, unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98), selectedTextColor: Color.fromARGB(255, 255, 255, 255), - unSelectedTextColor: Color.fromARGB(255, 207, 207, 207), - selectedIconColor: Color.fromARGB(255, 215, 215, 215), + unSelectedTextColor: Color.fromARGB(255, 192, 192, 192), + selectedIconColor: Color.fromARGB(255, 192, 192, 192), unSelectedIconColor: Color.fromARGB(255, 255, 255, 255), dividerColor: Color.fromARGB(255, 64, 64, 64), - hoverColor: Colors.black26); + hoverColor: Colors.black26, + closeHoverColor: Colors.black, + selectedTabBackgroundColor: Colors.black26); @override ThemeExtension copyWith({ @@ -1096,6 +1105,8 @@ class TabbarTheme extends ThemeExtension { Color? unSelectedIconColor, Color? dividerColor, Color? hoverColor, + Color? closeHoverColor, + Color? selectedTabBackgroundColor, }) { return TabbarTheme( selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor, @@ -1107,6 +1118,8 @@ class TabbarTheme extends ThemeExtension { unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor, dividerColor: dividerColor ?? this.dividerColor, hoverColor: hoverColor ?? this.hoverColor, + closeHoverColor: closeHoverColor ?? this.closeHoverColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, ); } @@ -1131,6 +1144,8 @@ class TabbarTheme extends ThemeExtension { Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t), dividerColor: Color.lerp(dividerColor, other.dividerColor, t), hoverColor: Color.lerp(hoverColor, other.hoverColor, t), + closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t), + selectedTabBackgroundColor: Color.lerp(selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), ); } From f12de3fec0034a944857503c9a8e3cb8bdb364d3 Mon Sep 17 00:00:00 2001 From: Mateusz Prais Date: Sun, 29 Jan 2023 22:40:17 +0100 Subject: [PATCH 261/734] Update pl.rs --- src/lang/pl.rs | 118 ++++++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index f953c5c0b..467d918b6 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Twój pulpit"), - ("desk_tip", "W celu zestawienia połączenia z tym urządzeniem należy poniższego ID i hasła."), + ("desk_tip", "W celu połączenia się z tym urządzeniem należy użyć poniższego ID i hasła"), ("Password", "Hasło"), ("Ready", "Gotowe"), ("Established", "Nawiązano"), @@ -38,12 +38,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Zatrzymaj usługę"), ("Change ID", "Zmień ID"), ("Website", "Strona internetowa"), - ("About", "O"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("About", "O aplikacji"), + ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), + ("Privacy Statement", "Oświadczenie o ochronie prywatności"), + ("Build Date", "Zbudowano"), + ("Version", "Wersja"), + ("Home", "Pulpit"), ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), @@ -99,7 +99,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Directory", "Pusty katalog"), ("Not an empty directory", "Katalog nie jest pusty"), ("Are you sure you want to delete this file?", "Czy na pewno chcesz usunąć ten plik?"), - ("Are you sure you want to delete this empty directory?", "Czy na pewno chcesz usunać ten pusty katalog?"), + ("Are you sure you want to delete this empty directory?", "Czy na pewno chcesz usunąć ten pusty katalog?"), ("Are you sure you want to delete the file of this directory?", "Czy na pewno chcesz usunąć pliki z tego katalogu?"), ("Do this for all conflicts", "wykonaj dla wszystkich konfliktów"), ("This is irreversible!", "To jest nieodwracalne!"), @@ -121,7 +121,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Dobra jakość obrazu"), ("Balanced", "Zrównoważony"), ("Optimize reaction time", "Zoptymalizuj czas reakcji"), - ("Custom", "Własne"), + ("Custom", "Niestandardowe"), ("Show remote cursor", "Pokazuj zdalny kursor"), ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), @@ -141,10 +141,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Nie udało się nawiązać bezpośredniego połączenia z pulpitem zdalnym"), ("Set Password", "Ustaw hasło"), ("OS Password", "Hasło systemu operacyjnego"), - ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcią problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), + ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcia problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), ("Click to upgrade", "Zaktualizuj"), ("Click to download", "Pobierz"), - ("Click to update", "Uaktualinij"), + ("Click to update", "Uaktualnij"), ("Configure", "Konfiguruj"), ("config_acc", "Konfiguracja konta"), ("config_screen", "Konfiguracja ekranu"), @@ -211,13 +211,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "Uruchom bez instalacji"), ("Always connected via relay", "Zawsze połączony pośrednio"), ("Always connect via relay", "Zawsze łącz pośrednio"), - ("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), + ("whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Zweryfikuj"), + ("Remember me", "Zapamiętaj mnie"), + ("Trust this device", "Dodaj to urządzenie do zaufanych"), + ("Verification code", "Kod weryfikacyjny"), + ("verification_tip", "Nastąpiło logowanie z nowego urządzenia, kod weryfikacyjny został wysłany na podany adres email, wprowadź kod by kontynuować proces logowania"), ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), @@ -235,7 +235,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Favorites", "Ulubione"), ("Add to Favorites", "Dodaj do ulubionych"), ("Remove from Favorites", "Usuń z ulubionych"), - ("Empty", "Pusty"), + ("Empty", "Pusto"), ("Invalid folder name", "Nieprawidłowa nazwa folderu"), ("Socks5 Proxy", "Socks5 Proxy"), ("Hostname", "Nazwa hosta"), @@ -334,7 +334,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Styl przewijania"), ("Show Menubar", "Pokaż pasek menu"), ("Hide Menubar", "Ukryj pasek menu"), - ("Direct Connection", "Połącznie bezpośrednie"), + ("Direct Connection", "Połączenie bezpośrednie"), ("Relay Connection", "Połączenie przez bramkę"), ("Secure Connection", "Połączenie szyfrowane"), ("Insecure Connection", "Połączenie nieszyfrowane"), @@ -347,12 +347,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark", "Ciemny"), ("Light", "Jasny"), ("Follow System", "Zgodne z systemem"), - ("Enable hardware codec", "Włącz wsparcie sprzętowe dla kodeków"), - ("Unlock Security Settings", "Odblokuj Ustawienia Zabezpieczeń"), - ("Enable Audio", "Włącz Dźwięk"), + ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), + ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), + ("Enable Audio", "Włącz dźwięk"), ("Unlock Network Settings", "Odblokuj ustawienia Sieciowe"), ("Server", "Serwer"), - ("Direct IP Access", "Bezpośredni Adres IP"), + ("Direct IP Access", "Bezpośredni adres IP"), ("Proxy", "Proxy"), ("Apply", "Zastosuj"), ("Disconnect all devices?", "Czy rozłączyć wszystkie urządzenia?"), @@ -364,20 +364,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", "Włącz RDP"), ("Pin menubar", "Przypnij pasek menu"), ("Unpin menubar", "Odepnij pasek menu"), - ("Recording", "Trwa nagrywanie"), + ("Recording", "Nagrywanie"), ("Directory", "Katalog"), ("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"), ("Change", "Zmień"), ("Start session recording", "Zacznij nagrywać sesję"), ("Stop session recording", "Zatrzymaj nagrywanie sesji"), - ("Enable Recording Session", "Włącz Nagrywanie Sesji"), + ("Enable Recording Session", "Włącz nagrywanie Sesji"), ("Allow recording session", "Zezwól na nagrywanie sesji"), ("Enable LAN Discovery", "Włącz wykrywanie urządzenia w sieci LAN"), ("Deny LAN Discovery", "Zablokuj wykrywanie urządzenia w sieci LAN"), ("Write a message", "Napisz wiadomość"), ("Prompt", "Monit"), - ("Please wait for confirmation of UAC...", "Oczekuje potwierdzenia ustawień UAC"), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "Poczekaj na potwierdzenie uprawnień UAC"), + ("elevated_foreground_window_tip", "Aktualne okno zdalnego urządzenia wymaga wyższych uprawnień by poprawnie działać, chwilowo niemożliwym jest korzystanie z myszy i klawiatury. Możesz poprosić zdalnego użytkownika o minimalizację okna, lub nacisnąć przycisk podniesienia uprawnień w oknie zarządzania połączeniami. By uniknąć tego problemu, rekomendujemy instalację oprogramowania na urządzeniu zdalnym."), ("Disconnected", "Rozłączone"), ("Other", "Inne"), ("Confirm before closing multiple tabs", "Potwierdź przed zamknięciem wielu kart"), @@ -385,7 +385,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Full Access", "Pełny dostęp"), ("Screen Share", "Udostępnianie ekranu"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga wyższej wersji dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po stronie równorzędnej)."), ("Show RustDesk", "Pokaż RustDesk"), @@ -403,39 +403,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Długość hasła jednorazowego"), ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), ("Hide connection management window", "Ukryj okno zarządzania połączeniem"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), + ("hide_cm_tip", "Pozwalaj na ukrycie tylko gdy akceptujesz sesje za pośrednictwem hasła i używasz hasła permanentnego"), + ("wayland_experiment_tip", "Wsparcie dla Wayland jest niekompletne, użyj X11 jeżeli chcesz korzystać z dostępu nienadzorowanego"), + ("Right click to select tabs", "Kliknij prawym przyciskiem myszy by wybrać zakładkę"), + ("Skipped", "Pominięte"), ("Add to Address Book", "Dodaj do Książki Adresowej"), ("Group", "Grypy"), ("Search", "Szukaj"), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), + ("Closed manually by web console", "Zakończone manualnie z konsoli Web"), + ("Local keyboard type", "Lokalny typ klawiatury"), + ("Select local keyboard type", "Wybierz lokalny typ klawiatury"), + ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), + ("Always use software rendering", "Zawsze używaj renderowania programowego"), + ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), + ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), + ("Wait", "Czekaj"), + ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), + ("Ask the remote user for authentication", "Poproś użytkownika zdalnego o uwierzytelnienie"), + ("Choose this if the remote account is administrator", "Wybierz to jeżeli zdalne konto jest administratorem"), + ("Transmit the username and password of administrator", "Prześlij nazwę użytkownika i hasło administratora"), + ("still_click_uac_tip", "Nadal wymaga od zdalnego użytkownika potwierdzenia uprawnień UAC."), + ("Request Elevation", "Poproś o podniesienie uprawnień"), + ("wait_accept_uac_tip", "Prosimy czekać aż zdalny użytkownik potwierdzi uprawnienia UAC."), + ("Elevate successfully", "Pomyślnie podniesiono uprawnienia"), + ("uppercase", "wielkie litery"), + ("lowercase", "małe litery"), + ("digit", "cyfra"), + ("special character", "znak specjalny"), + ("length>=8", "długość>=8"), + ("Weak", "Słabe"), + ("Medium", "Średnie"), + ("Strong", "Mocne"), + ("Switch Sides", "Zmień 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"), ].iter().cloned().collect(); } From 6db94983a181a942474554c78b9fbb554df21f24 Mon Sep 17 00:00:00 2001 From: Simon Spannagel Date: Mon, 30 Jan 2023 08:06:48 +0100 Subject: [PATCH 262/734] Remove wayland fix for good Signed-off-by: simonspa --- src/platform/linux.rs | 93 ------------------------------------------- src/ui.rs | 10 ----- src/ui/index.tis | 13 ------ src/ui_interface.rs | 14 ------- 4 files changed, 130 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 34276426d..ac3b32a49 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -426,104 +426,11 @@ pub fn is_login_wayland() -> bool { } } -pub fn fix_login_wayland() { - let mut file = "/etc/gdm3/custom.conf".to_owned(); - if !std::path::Path::new(&file).exists() { - file = "/etc/gdm/custom.conf".to_owned(); - } - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - "s/#WaylandEnable=false/WaylandEnable=false/g", - &file, - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("fix_login_wayland failed: {}", x); - } - } - Err(err) => { - log::error!("fix_login_wayland failed: {}", err); - } - } -} - pub fn current_is_wayland() -> bool { let dtype = get_display_server(); return "wayland" == dtype && unsafe { UNMODIFIED }; } -pub fn modify_default_login() -> String { - let dsession = std::env::var("DESKTOP_SESSION").unwrap(); - let user_name = std::env::var("USERNAME").unwrap(); - if let Ok(x) = run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION}-xorg.desktop".to_owned()) { - if x.trim_end().to_string() != "" { - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - &format!("s/={0}$/={0}-xorg/g", &dsession), - &format!("/var/lib/AccountsService/users/{}", &user_name), - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("modify_default_login failed: {}", x); - return "Fix failed! Please re-login with X server manually".to_owned(); - } else { - unsafe { - UNMODIFIED = false; - } - return "".to_owned(); - } - } - Err(err) => { - log::error!("modify_default_login failed: {}", err); - return "Fix failed! Please re-login with X server manually".to_owned(); - } - } - } else if let Ok(z) = - run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION:0:-8}.desktop".to_owned()) - { - if z.trim_end().to_string() != "" { - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - &format!("s/={}$/={}/g", &dsession, &dsession[..dsession.len() - 8]), - &format!("/var/lib/AccountsService/users/{}", &user_name), - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("modify_default_login failed: {}", x); - return "Fix failed! Please re-login with X server manually".to_owned(); - } else { - unsafe { - UNMODIFIED = false; - } - return "".to_owned(); - } - } - Err(err) => { - log::error!("modify_default_login failed: {}", err); - return "Fix failed! Please re-login with X server manually".to_owned(); - } - } - } - } - } - return "Fix failed! Please re-login with X server manually".to_owned(); -} - // to-do: test the other display manager fn _get_display_manager() -> String { if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") { diff --git a/src/ui.rs b/src/ui.rs index b8473072d..637fc66be 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -434,18 +434,10 @@ impl UI { is_login_wayland() } - fn fix_login_wayland(&mut self) { - fix_login_wayland() - } - fn current_is_wayland(&mut self) -> bool { current_is_wayland() } - fn modify_default_login(&mut self) -> String { - modify_default_login() - } - fn get_software_update_url(&self) -> String { get_software_update_url() } @@ -590,9 +582,7 @@ impl sciter::EventHandler for UI { fn is_installed_daemon(bool); fn get_error(); fn is_login_wayland(); - fn fix_login_wayland(); fn current_is_wayland(); - fn modify_default_login(); fn get_options(); fn get_option(String); fn get_local_option(String); diff --git a/src/ui/index.tis b/src/ui/index.tis index 2d77b1eec..e718e4380 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -755,11 +755,6 @@ class FixWayland: Reactor.Component {

    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 literal 6186 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 UOVY_Hz5j7-4ji(4Y~hyhKatt@b^rhX 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 64558031098587ed42fa4c3edad58b95d7fe67bd..daacb85ae376f30551702e9ba0ef7bf56fb981a7 100644 GIT binary patch literal 2618 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 delta 889 zcmV-<1BU#%6om(n8Gi!+005o0f$RVP0Jl&~R7C&)0043S0CE2SasL2u{{V6S0CE2S zasL2u{{V6S0CE2SasL2u|NsC00B`>kegE+H{|l;|MmL+v(^8N!2j6l z|2&ibDTx2&@&D51|4f_zHjn?{?*Go@|166CB!&O!^Z%^T|9_&)|9`muWvc%gfd9nb z|EbUag}eVkmj5w~|CPl5h`s-Fvj1?f|81`SQl9@Fg8%&f|LpYtzT5w}*#Dcz|6iv6 zS)%`UwEqO5;q?Fj01R|ePE!Du$&iUDALU{)uGU0&z)1K~#9!?Ux0b>mU?H z#})p#*WKN0|9|V6X(8pc$w$a@J0sP5aJe`C`OSEIdAL>J;r%f+{OSIgDDr&&l#GA9 zBRRNxO_a~$l?~T*xFdASfgkR$;31JVQ!7lPZAa4jerAn43X zTAJckDu5Mu702uo817Q9c!7;P0+67m(@!b}!aDs#Z+{F3lm*aQD|(`!VW`s+x~D-= zfSOA=J`*t3RVWD%c3A+cL3Jfwm7pNNLd4UCg^n*?R;tJp!06GKa`PBabn7%`qajl; zUB>M{)U4yzVGqX<<+QSDfUry%y%0E$YH33m^%62_;8DiNz{SAE4cp3VfEuqrgU839 z!@D*LA%7K2dH3<|7GS{V!%Ql$d0Sf`*I3PXWF-RBB2mvPP&C=#bRZG%FS!;;fCZaT zdPyeWUC;+fVz22DvnUgIqN9@VPT0u=Blewks$eTVlndlFkPF6=KCsv>vD_b395BARQnV`i=OBj5`RHF*3`9;m`AJy)RQl!1N)Yl97sMz zoT6M1MtnI27Y2~FBU+%eq4-}OB(Ntsui6e7TUyQMhI+ro6o4HxOv-SL3<1I35u5Vd zCIp?ft5Z%?Lgu+(jp-S7xQf@#4hjd0!50lbT!*@dpQs&e#>o8QXba9w(q~q-fcJS_ z9CEHDJ(d;Kk9QKd4r{{saE-hL)C9Z|#b((&9j;o1Ql~H;5|D&IcFBTOo3~ma2noE& zAnlU$kaf+&5%HMd<(n^>-+I;k=F9fCU-y#<$6Jns Date: Mon, 23 Jan 2023 18:50:25 +0100 Subject: [PATCH 220/734] Use OUT_DIR in build.rs --- libs/hbb_common/.gitignore | 1 - libs/hbb_common/build.rs | 7 +++++-- libs/hbb_common/src/protos/mod.rs | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 libs/hbb_common/src/protos/mod.rs diff --git a/libs/hbb_common/.gitignore b/libs/hbb_common/.gitignore index b1cf151e3..693699042 100644 --- a/libs/hbb_common/.gitignore +++ b/libs/hbb_common/.gitignore @@ -1,4 +1,3 @@ /target **/*.rs.bk Cargo.lock -src/protos/ diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs index 225ec34c7..bff0cfafc 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -1,8 +1,11 @@ fn main() { - std::fs::create_dir_all("src/protos").unwrap(); + let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap()); + + std::fs::create_dir_all(&out_dir).unwrap(); + protobuf_codegen::Codegen::new() .pure() - .out_dir("src/protos") + .out_dir(out_dir) .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") .customize( diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs new file mode 100644 index 000000000..c001c58fb --- /dev/null +++ b/libs/hbb_common/src/protos/mod.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); \ No newline at end of file From c8058cd125e843f4a551e0c8179c1e4647807cc8 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Tue, 24 Jan 2023 08:08:53 +0330 Subject: [PATCH 221/734] Update fa.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ("Group", "گروه"), ("Search", "جستجو"), ("Closed manually by the web console", "به صورت دستی توسط کنسول وب بسته شد"), ("Local keyboard type", "نوع صفحه کلید محلی"), ("Select local keyboard type", "نوع صفحه کلید محلی را انتخاب کنید"), ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), ("Wait", "صبر کنید"), ("Elevation Error", "خطای ارتفاع"), ("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", "همچنان کاربر از راه دور نیاز دارد که روی OK در پنجره UAC اجرای RustDesk کلیک کند."), ("Request Elevation", "درخواست ارتفاع"), ("wait_accept_uac_tip", "لطفاً منتظر بمانید تا کاربر راه دور درخواست پنجره UAC را بپذیرد."), ("Elevate successfully", "با موفقیت بالا ببرید"), ("uppercase", "حروف بزرگ"), ("lowercase", "حروف کوچک"), ("digit", "عدد"), ("special character", "کاراکتر خاص"), ("length>=8", "حداقل طول 8 کاراکتر"), ("Weak", "ضعیف"), ("Medium", "متوسط"), ("Strong", "قوی"), ("Switch Sides", "طرفین را عوض کنید"), ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), --- src/lang/fa.rs | 70 +++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index dfd76405e..b107bb91a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "تأیید کنید"), + ("Remember me", "مرا به یاد داشته باش"), + ("Trust this device", "به این دستگاه اعتماد کنید"), + ("Verification code", "کد تایید"), + ("verification_tip", "یک دستگاه جدید شناسایی شده است و یک کد تأیید به آدرس ایمیل ثبت شده ارسال شده است، برای ادامه ورود، کد تأیید را وارد کنید."), ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), @@ -383,7 +383,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "اشتراک گذاری صفحه"), ("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), - ("JumpLink", ""), + ("JumpLink", "چشم انداز"), ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), ("Show RustDesk", "RustDesk نمایش"), ("This PC", "This PC"), @@ -403,35 +403,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), - ("Skipped", ""), + ("Skipped", "رد شد"), ("Add to Address Book", "افزودن به دفترچه آدرس"), - ("Group", ""), - ("Search", ""), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Group", "گروه"), + ("Search", "جستجو"), + ("Closed manually by the web console", "به صورت دستی توسط کنسول وب بسته شد"), + ("Local keyboard type", "نوع صفحه کلید محلی"), + ("Select local keyboard type", "نوع صفحه کلید محلی را انتخاب کنید"), + ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), + ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), + ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), + ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), + ("Wait", "صبر کنید"), + ("Elevation Error", "خطای ارتفاع"), + ("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", "همچنان کاربر از راه دور نیاز دارد که روی OK در پنجره UAC اجرای RustDesk کلیک کند."), + ("Request Elevation", "درخواست ارتفاع"), + ("wait_accept_uac_tip", "لطفاً منتظر بمانید تا کاربر راه دور درخواست پنجره UAC را بپذیرد."), + ("Elevate successfully", "با موفقیت بالا ببرید"), + ("uppercase", "حروف بزرگ"), + ("lowercase", "حروف کوچک"), + ("digit", "عدد"), + ("special character", "کاراکتر خاص"), + ("length>=8", "حداقل طول 8 کاراکتر"), + ("Weak", "ضعیف"), + ("Medium", "متوسط"), + ("Strong", "قوی"), + ("Switch Sides", "طرفین را عوض کنید"), + ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), ].iter().cloned().collect(); } From 374167b7827a78c9f738c5663746c3af550e3da0 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 24 Jan 2023 10:30:29 +0100 Subject: [PATCH 222/734] compressed --- .../AppIcon.appiconset/app_icon_1024.png | Bin 100419 -> 23562 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5792 -> 2409 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 569 -> 338 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14429 -> 4616 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1256 -> 644 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 37270 -> 9733 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2618 -> 1222 bytes 7 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 4b6ea50696ab62a80cf4eaa9ce6d344f8c478917..1c6cf008ad8720ee4680a32de29836e8d31ed7fa 100644 GIT binary patch literal 23562 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 literal 100419 zcmeEtbzhX<^Y^uYq?8B#NEgcI>Eqh=1 z`96UA0sQ{(<-YXHoHO-4b7toHuB<3ch)<0V005!PTL~2azyg0_0l3)UU$93k{s80~ zzKq0cH4n({0ztgqy6@%%8ZF|w=PhTI{skJs`qGlhpMi!i^tFO8o=j?aNL+N6ViUGd zsEAv#LwR$(jYCSGFt^Z52Pf6A2bTcV!0uW?kriW?(V6GeVnQ9L~{}1v`v791HrBn`ohfE0`(L^N6Fo4}m5?3j)*Fk_SF;x!ltF^L= zNk8Hm%sZ4A1Jxy`9eZvLFPS*737nn6hERu006gvq0E|qDnuwkL>Xi7>EF@l9Y>Zyx zJ{JqF8~}i14i4-CrePfcbs^SxYkh;e`151mq;o`)mPb!SRRDbu{oW>?Y5Ta-|0yXI zwf#6ukzdxRBTCaIaR9)BcR$`}qhq%<$eotx|3j?aj000f6 zkERqh-CN)5uPm~kzc_;cP$ZR*W7DxQ{qip$21gMK!}*AaxUKh=@AA-b;rs*rMbDME z=eLZvB<*5X8Z%ep-BWO}W>J`GEv&5v=J?O01)#y;`Z}M9P&o~|5WmMQa#KlXM=YcO zfQT#9rDUn?PjO-`KralyIph>*BVolSj>JT7sWE``C<7hDaEHi45I~5Wn1t+tLjCQc z^jr=~D1X5@$gqKw*PzYp@1wxr5*w(w#GaqW$K0<%N+apQy%8VlzTQD_V*txdWz6n9 zsatW0b6h_}n(4sBKUNiE=b7DSUpXpZ70_{*>^Jiza-@eC0N#t&(J!}56$|-aNe-fRp90A= z;GRYGm8emU)G%6_xa+Mwuef>k30$~Hi#tTJ2LJ(2A>>0OHYt4q?J#4_jAN(&} z_ugPF@4_l%UCtr6bSpW3>wHJFSuSvta|ScA97c~$UWc?LfbKN}wN6AcCBFNUcIkqU zvtZmm0%dwSuU5FJ!qzZv*7`gndX3gIkP6bq0*Yl2q3+(*8p9=Lr8J)f-s5dAisl)q z_4gw=WV>S5(#WuYAL0OT9iF#^%raykIQA=_3fs2}^OgXadXTf@=1Im0wO@5sKD!X{ zRFPPvB_B}-%}F-T19LBS?<;oFyWZMx^Ron3BdDDVv5Ese7Sp%glnCedaXwlm0RZF4 z*{yaaJ!i2C7qt`6nxFtca-{}Ix_d9`*iknU_QH9TGcB(|80eJ*FAC=a)H4S6s;P6B zU_(1K3^e_Xb7A%zHwHpgrD94npIJ5ycuEQZB6}5(=xBKvKPULhK2+*3$m(rrlqCCuf)S#B0t4I*@s(IkRA3lhS^)&~aL% zyRP--yuS_2H@`OkTy)IOeroC4Vw3f;q>--JtzMf8r!hP;C!Zc7^;bW58#4p10WUfT za0$Iqm`S;qR_fV%ccc{cZ)u^@j{ccOebyP7lhVG7bukwW&>RW?BE=}FhYNKkdtA*d zo4HrcQ@3ZJk*KXr!s@e^cK4|*R4acTOAj;$f@=qOExwoRhr3JL55QQHPeHeByEw9J zmRsa-+uM`u6`7een4NyP{~BTu1g`FpE#%z!O3`Izn)lq;k9QxmB9MMi&xVPtb;Ez~ z0!843nNCRxq<7cx&qi+Vb&7CtIT<3_&g!H={D*fuAY)03`LZ&3JX#Gi7EAMfGx;d( z9!5Gp36L&bHFO!Y&qh;!zCS0q&g~d>o0a03t@HJ+nJl@(5fq?b!alP2aw*X7gHcrQ{N9Gj z==!bR7wlx!&f;npj^{@AygQKkCsw3sW{@pfEMV)&j&8ohet5rx^n5hBaFym(0tbyQ ziGhP1{n678B!$*4o$kCGA_N=VqqKlCt{j zV9Cb^Bmlh>b55i>tft6G^H1fQ%3FmZ()LPTrK5dNaoN!q+%%S70oFUvO;UiKQ6op0 z@5YfT#`aF_B1~zX;gAj`MRlo)zG{aQIH_5E>F z<&x9)N668v1Z@`}kev+APBq3SopDe$I|r`Z8uR;)4Nd0hf#5!Wq+i}sI-)6l>_cB` zAH()|>93-<6k8IX^3AgByv95E4cQXi2+_}Pzz6|iko~806zHGcVrtKWzc#v&D<-2X z7=a;T3?P|lkDo2>eS^@=Xbk^%{zJnKS15>DV2Xne@%x*2+?P>%aPQSNt1> z_;af&hg>}+xV5>d)EXsAO!KJGS<47pvB=TW*T4`S1mJ^DMzUO^@(xSYf}4({ImqnS z&=i3%kZyT5zp3>&i~nI7X?>v@vt%c1l&y4f3-o{I+dt-W_Tn|JXKDYW5OANmmR(Pz z2~{^Qd{)~vh14d3J4=JBOy(@mx(hcP4SrD`lsD2D=U})1Ln(=kW;>15$|S9N?2=y> zB@4G{NX3G^KWr&r0RlKR)f+H39TzFaDyQ9FVpg_Bw?6vbB!0p^ruHVxnag?Ek1giS z5{GQLmZ>|?#E1oWgcRLB`@Ts>BBammjZ?F5oRD`8v-mG?E<=xQ-j>`<)Z$m7HBks>-JV_SouhF&s8<0f*Qkot=i8?=Rx|pG@L8FK_Gahc3p5(jt4nZQ(6u*e#cv zQT+NWF_=pGi7zBNVJ`&PIZ}T=hLZpm0RUi;p?=-CccX@Hwjd&Ee`fECEDhvrCI63F zlwZSnu*SUwE!e8!wTRR|;$R9L)<=~(h7pUvL0Wb%LrYsE)3b#9{n5S-h3RiHD1-U- zTY2vpH{Mn7RXm!%j-w2NHE8*V_A9(v}joPqy;7UM3hjMdX3@& z+rN;7`pm1$7av?tt(?0v$6Q!lr8h5>5(uz?C!G4eVyM@RPT2koUTJyft?PE2F4woV zH@2uSJXhR5PK#zb8~(meUVZh({cdR=D3~xvCe?WEu7JMY@>-IZd|0=f%RhQ_>%v>q zbJphPVv71P_4{l>_(s}t%rzf6kqRz&tF#Xrh#`PM>_Je$&N=^1&D9(!IhtQl#(WL^ z&07zo1r#$cyXudSXuIvh*2}ehVx<6YWUP2&>4O5pY&yUX7o-j#Aj~5EQZaLDKrxi2 zlmR-+kYwr>z5x9mpX;N3F+>j{1APsx_k!xv0hf^FbrN7X$Hu!+Y%+3acQ$KyFt9JC z_F4ncL+D6M2$Ho~denOp+teQ&xP=XimBTV^s?^GA7D(v#BQk<+X=QWHbR~>*ScZjj zokqP&NR1Z6K(dhq7qyC2LxCG)bz1c1MvgMcG-i7BE9g9fIFcS{BhZ6tF<09kL{+#$ zZ~hR;gzN^ntAS*|&^NDXF~9Y0KP3$`Qc0ayt!TeEaqB_rkwgy*y9mD>KseR04_#oM%-?e|$(04J7t+2K5{dDbxIR&(W6ilN* zFEDe{vB+VK$*~ZrUyJSU=tr5)o7^h)kUGzxA-R67HoM_Ee_?eD!4i|Vm1e2Kg20j& zzyQ8+NFEiEvl$-6_ievZ!e%-k&%L=NYnE#^jQwtSJ$zMYcoDRqDa?4qRFL}ivCS~n zdkUxuxM{M`m3@%pPGcxK%uLvG)!s+g3vpT3eGBEw56z@+l)IJ=O`e23jM#<@q5J{* zd_@~PE7Vp^%f;dZBGl2>=h8NAvYAH}4Tco_rc58(Z60-9-db<%Wh^++yq{$EFhM+|W$B-nzU+cmxgiq5axe(h~C$Z297s z&8zz_@(zM1f{IJvp8>o#K={k423K^8V`r-_P8IX`<$2C@vm#IX#4T*6)8!j5ua39A zSV9FUiE@hCBf28{fF>W5iEJ-#`L9#ezUH>t2XvetDzauQZTkE7aBOAGhcel7}R4 z47&@Gw|T}=p;P?3{nH`{th!(u1`%Ul{oRg+S>f6Az+2qu(^j>6r#Vl#^zUEz^jbeO zeCc%50sH1{bGx(V$D*lFDQca9sm84e>e>&|ZjSdL-%DWIZAN_JWgq&W`yXjN~L zhCSyPr4Y+&(l<>1`2*fc4QkP4Ax+-)V&i0ZP@|oy^$GdX7hnM6Ed~Z0tE>hk$Q~m> zQR*S@F3NU{BCwlz%!sDp{1>E9>A!h4sW*c;2fpjmsv$nj$IDF=`0ADRhJQMy;0Vku z3oHOHU59IhQqsI%kZrZ$fpAR04@S#>K=xGS(u9DrW%J~(IC?s&Vl|wtzfMRyZ>pHa zP4(+&EiQ1Gs(vKL6bL&asw+XqHC=IW{!I(>HocX172d2q&SS(h86qwS!l;mC!_x0)?S>n7XREiyPpUhfOQtp z9mc!2U&z?LZ9+EcU(5&aVC|}c+ZI&&bPq1gurzr4EIv#%RJ6?x$qmi!zH$98WJm7s zoUT@!z;5nbTn(ceQA*SDh_}mE8oq|V>b?e?_yg%2)jw`1&ter^r$3AGv*<+Sx|?2k zFG{c89vXM_s)^F2UwSCpCM9`>uZ3MM7`rPG0PI)<_2fWQExq3{H)A3w&fqx|Kf}&ftFG> z^3rja`FUCS?BLft>A~S+>!-Z`qvo(s>sq`n2j$vMqt(;@@K!Q-;O*~Jyf!X4_-q`p z&^u{Wk=bpTagvSYvwWV~7juBV^Uhi3e+mPWS(uOMa)4MgozMEKna5JJ$!b@GmJQ?R zm0Ar<5-bD+1KwgZIqN34IUJic{TCAP{-t^``;WDipN2u+F1N>Tn9b8W%nj>{_$KDL~Z0D|?6>^i+>Oom%l4R0d}osjpT- z2!j($^5=)UMfh${Eh5oOhb*}qIb9VyKPfFX`#M;&EwKvCuu&s;TzUHnho&9+zC$N? zGE4^B^nTgs=@`~$V?3Su59ZTru@cG+g(}Jp%S=N9>aQzP)xxl!;v@Un&p*sF+}^d|*q{I?X^$bzo?j>)0sx z_WtvyngQ6d+^a_CL`rnFdf*VzBA)6UuMv9x=rFx`H=p&7K8tJhAf|Ku>ocKyV0b&2 zZ!oW3<&5YQl9~gE=NRzR+|k0}g8O|9&VRL=9Xx{aCnq1$_$-Kp{)Y`-TFfWoPDj;A zYZs+-r1ru^@@mBgluF<;g$i2w1lTfS_hpHZK;di-uC`0Nsv0zzI0-*XyqT%%zU_Ub z{|M>3BX&mm>be-lczP!${q}X@%l3PU$aEKXBnlVs(m*86>GBmPQBDlhWLS!LYt(Xw2WQL`B9(ZySZr zdS0v#j&4?55~17eXq*uA7;pnqeU#z-R-Li@{5|vh*yB*s;)wET81sI{t>bPn>|?EN z)P~M&S`-H5#(L>hoT4ht$k9->=qg5xmjvlwNdUwHFd@=(9Je#|GM*ivOW>D|)et_? z7|+b73-k85mEm$=ZBF!|FuHij(ql$BfY?(-AfTlhuM z%9eUn-19iz2)hn`KKL*2Y|7NmEGTx)-1*nD=7Nmf4pJvjD)*K+tja$~#%Kf0pMZ2^ z9-Z$YQwI;uSJ(d0*RLW)Y@YI7hON*(0QZKAW}C<9of6m!33{WaYu1j>F1opl{?OeQ zhuQa11a;O)f31Xqr_dn;2*8)e4e{Ph_YLE-YAeC!lh{2%j??k~GBjvfUnn~Gb9JjQ zR~dD#vq|o?;Xdz|UoLqCM)TtB37|Vaw!0FWR8hNDN<=OB2~=CMnuYd-Ti&&67TquZ zr%MyYMonrb8;--q!QgZuD6g-K@)Nl8JTgn9U0F0|QFTUBVSw+M&A;=s{Ud zWc+psUP_|w*+#5}{5XMXzb@=jh4U}^@&3<$$8f$NI3o(pL38O54y*=si_AQ(VO<>) z)$Y9uHvlyuq{9s`U(s@n5)8g&-egOxVq%CZJEgl-<;ie1%*%5;EQF8_WiMT$myGO+ zJ^p!B!lDJ!R4Y6$xnaWEa>D{d(wol=P2DtHHs?wwJ6qEx+pQJ2<0}IG{iL_R{~(S2b<=>-xixH#43+e_}K`sG|{x zwvz%2c{UxcXe5{_tpz}xAI2F?QAgZSicKYCi3vPH zQPp7XI$;3|N){IV%yPWk>m&K06x49t{!?Ql^Ua5gzo~P9D)NU|s zfdu7}dk9c`*%H7RM)Kn|Fw&`qM61YD>4~JPs?NFo{g79qJ#JRHB4c%;06v!g@yMTH zq(pa|Wmv5@=I_R9+nI~hul&m*7Bq2v#P%U8F1SF0M&ZW*HVVn>z_-qx%3(#qMgd7L zqT;GbyyVfh-bfNP`iP%x5Y-vQV3qXM>T9rYb8pY~XCZ&fOp64_NDjy$R*Hjmsg8}; z&5$#HSeUzXOmAEnTcuFQuB9NR;nowO562k95|Ao}T6tfjs6AtJ#&=t`%N<$`tEMRQ znVik|;rrsCG7Bd0^g|3@5uRl8@7f+w>~Shp=xU*FMWGknR=`=+3?-yZvt|9EnZy-g zx1T1KT|x}?rr`!n0isKc&CVB9Rw;hkCe2y}R&OJQ(xAdZ1Hb3?8rt zx8M5qBT6Rv+-YfvYCVD`8B*8yzxd^tc5no&5I~@mpi`(6ue7R-{e3j9Q_R<_ylwu{ zaC*J^s>#y|MIq8DSPsU2iO~skBy(XW*NiM&6|Mq?`x01DM z2#)z>GdduvT@dmtCZj6iPEw@)LDt2;=sM5AE{!RRjVjYwA58$KSU28C>z`yfiICfxdI~%+* zECXIBX^1N|4OIWv}`X?9ta}ANcrt&3nD1n;q+W)NlTL7kk6q#tvqDu0?@{ zV@{GTx2P{icVaWh9T}SHg)$iW_dMOAU0im3M;I09)8OagI1{CANjvKZoews*CdlA| zq!*F^E|2(?x-_+uC%Tzp2Xk|pW_=LLPVG^Mt&;!n5=@g-Gy`QkE6kfBGOF0KrGnK8 zr5Lr)OCg|dDtdR{g&~`r{^-3Ynx-5`zw1$`A}3=W{$GSPd<*r6yWS1MQW81e`X#&E ztK8RPg*YTTk&-L^i)MvvJpuLDCB+fO4_h-(N-arfuOGRKL&fnnN1ei2N`6Cng_aI@ zI;dr^mKc=PuMtt0)ju$FX0pHXFo?)$BeaIY@Fw)_y7%6kfV*mvgq@#%&)L!0-f3z5 zYD?F?HF~0{Cu_LMUhLBUybB+Yp}1?{qt705rNL~*OSUjfq>xyw`Svh$tA_66_u;KY zJFW7cHlWMJHLMuS`#<;S=m%F%_2h8JeM<}i-3EC2)9cSg34c`(I*MRewV8xOA}*=a zO6;r?&8~9E{JPEhPfKd;(#XxFp>u5O>6AF&?z#2-Y&uODd{5HHq#0OC@ydM|d|Q(s zi&Om`J+578((*8{nt5*z7`X$gGU#+clqx8SIV#gAD_1T-lA)i#l4?=(5po$wPpjv? z@;0Ecp0dUP|>L#uvDrFPZ{FjYW(a`J`MA2nTq_q`o0e(Nw z0KSu^%b30UrQ9Bl)mvaUj~4OzTl-h&*?Tzk@(==$WgfksZgaW~tE}h*C)LH2rvX4` zlEF3l;0}A0w*7X`F4)+-!-!%~K{idQiD#`W$$O9;pO+^_T$(;FIS!MB<jM{)Kf!2TA&LAX+s=P$-?3 zdwJ(GIMA??Q!{0wnqxHhekiek-RlV@-?OV|SKSoKAXdyRY%t*3bP5Zer5qt8q)DYu z%jCBl+-6v*ttBd@xWuQsM5KUkx?=ZHKV(&KM|L{V{jciX0V6P#cGw=qG7PUdmV(>h%pz?+e`sUPR z6vnK1zwy&3c^Jy7c^oFj0A>Uc=5|+MZ%;~F`NL~BJe(DBT8geV{7 z8_fF^8=2>$M8gw|VeV@4@u>MbVEQwnsh4qp-#%BWG;|-$y-vOEd$fa6z289f=-i0O zp6z^#SmI{cC>9skB4ezsru`NX&AURKSgP$@*09`59;Hx^guXqZ6T^W2)^^qBbccD2 zy>*=rEq%y$vj3;6Ai|~VTihZAn3}w2sLR}(s&%MAPtGH0BWT3vkk9$LYl};edpLn!MNL-0lhZKMS%ArD~>1aBI$tWcU1`%3p1v_o0p%SJ9$^NUJW&hDi+f5 z4WM_k@?9!GFE>Ala^-Gii*6eMx?>P9fuQ| z724MauROoQ=ctcA9w2UpA@DWJTq&szkHtXNA5U3A*y=|fo^LRC{j3?B-3Hp8!nH_& z4tB70IeCYdL9XP&kg;0Nkk6P#>X!@R_$3d3HR7x_2eZ4(paeb+TW^~8*J(o}z3za+ zI+&DtFFvATJ@$vfdrm&~beKt*K2ARm1HE{6qHkA}p-@_P&NSF`+%2OdQlU6Ohok|z z99Sof3t04#v)kFW3I)mhLC@}a>d!L>xNF*ncw;pmtQkPqanJpwf4>Hx#BVoiWt92r zZ5FessYJN$#h@176+cW2?n6h`n2TLecZm#SKLNg_sLwppV171Jp`BerLXJ9>RKms< zp1C<=S*3jhB;(~?*t6EMH%*_mqs95PmlPd*J>k|Acz}Lyzy8y|O(ndVgYTQoO{K)tYD{?Tkz;oNuo-Lqp{mK~{JT}-OQiOF!AHyPl4L0|ka zSF%}pdaM?vc;(h9#tj_`qm0FwpV%i|b|ZQ33BXH?x$N-Zy@}DY(MM8Ngi%yPgAvs- zEMFXJNdXJAi3C8xU|nn_lU;r1y=&vc7)h$WAFla-Be^&usn)OY;3cd0LetBG(U8ZfODK6X(10C+5-f~s?V3KE zE6kd@!Xg0~<98+5+_iZWQC+|8qcgCwlF6DImOsJHRpABagL5(f-(qGfc7sZYd%(1E z_9t&lo9hj6jvXd>)E^Rj=(1%Sw2iq5qqb4WI zGPugLYlE?zvM;N4TUKO!yMyx%xH0M3v4HZ5cjbnmB|p|jR3?K>D@Rf4<@tFC`)~Q) z^}V16(bt2_d1u=VPxe+F!{}$GXt3{L0pH}p7rY!vyp&|$t0hi7)Gjx7I*Dlz^RjC- zjC6aH{Tbk-2ez=~h6{r4ZvXmKK+3T`nP}A!VCQvL0f4wX`~q{2-~RI?F)e8y3XT`> z&5RbSgq0}BKC3h}WwRy{LF%Ur85ZK;uQHpN107&b!Jvl3_jxH%A*(P`YPM)d=m--G3xZ`8sn`(xQsgJ>W!fSHx2^zgq`S(uuawT^R zQmMf19F2P}x&^6x?%udJiu{{T%Q6%%IVOY)Haz+B=gIF)C}@l`Gt!8Zjd<00{wa&y zB+5I#*Co`p2F7#<7}IWEHAN>}94w;@-Tncx52!7v8S3BF09a*z<`ZUC6!x%TTYjsf z$esK;97hnoy~4mW?ZJ6xDVRLoLw*4JP~6Fi>s1syAw7W+TPESe1M_<#8qMV9CP znTd+~lorNrCY%Q8c*t|b}t zULpMrlB<6X>%lTW_S1d+!NFcH#uxvvriJ#Uk+TpCCv_HLt8a7{R}j8u#RZJVW-luQ zGv@RtMtXL}wj*Q8?5tK~bX0Rmk>CaPH1`Vg_Xj25Nq~f4=d497UxMFI%+72-f2t~8Z#>IPH-<&hM&&q; zT9~OfV5K*pgX8#4%~f?%xFO5rBWuJ^z29s=TcKPCkCoK3JSx06 zSFM2cZ`L=h4chL8T=p$SmKYCkFyIfWEE=LEZnzri+ZSkKKBi}|OvQUW_+T}@6OdFM zN&>BQ##!LG*nu_I3~*k3jm=luM9@lSEDb zO?PT0-g|jmU}f3ww{OH>{c)%Ft_G$K+L{^~h8_(G3mpg;z_F7UG-rWY;00WQP8mNA zU;_>n0fcHrKMG$nG?Z9-J+sK>D~$|UgRVy_Q%DP zU$V!T>j`SQM1+Bb?vzt^oEcywB%Tmhe%6G@*2+?u?f^~12H0(Y6#oM`-gmYEsCXUBG@88QscYg%O z%kEg(!yX$pz%aSbdg zfzko3FsMMoQx!HRn+==%Xy8^Y8UrgA(Z%1+*D1$xP}1X?`x%S`jPYUkME?5 zy`%x&KIauDw0z6>l9B0^HU*1`$&;@dErncS**uWo|NP8g;NeKhg~G9iiMFm~ye`^a zIA$u~!J_iK3D$^b$+F?nBC*331n#$DjycezxP}SqvjONyC+^BTm%@nP)pX@6dgUh2 zAm)?Nvs-blqG3(&cn;!5@y`T%t2o4oM?V{e8UcF#Y*!jBJQ}Is?_kqTaDeyRIppT~ zIe_4myXFR}@moFtYAg=NG{t;yB;q=z9!UYce>4#GR&(fYrT}fF( z@>ey~AYdf(&m0+P`%Z1t%aFfvv|M&LDCYd&t!TD|IXgFgb(te{8@^B;3N z`cBmB^<*~$)>7M^GP<0`Z0{a~19p9_)(H0#b;mF)K=2?`WDhAujfH{gKM}6QfOcRr zvq%M0669X`S{EdJ(r8|#>Ni&$2VJ1SEdU!T}GD{~Su$*gr zZNS0;g!uGog0>Jcqn8LRD;EcaFgns+Gv`q^QZ&S};z@-f$waz#B z83fvjr&6Dh{y38NiGdfrUfyJ;Zc@&xh^aoC$-!@Kr(!}77ZM2ctG2beTxKI!=>{Ky zTSw#(KYeuK+Ib^hDgfAs@>74kS|&blMn~%&fXEo39&K~xw{1_%pwE&U+~nmIFf+U8 z*#EEvw(^LqQQ=o$Ij7r|i$Q~e_-{on?`@RnkTgTz%n|(p5M=y=lddi1ePTng1fc>9 zf!SJy_Z&7ccR|Kqc6%hpKD-^2qv9;&n%Kvs&uUDVDeo^oW3V#;xeCQomCY1h#z;W`xq3jvSJgYIoFRxmY65YdSpX$jg}74|b5 ze0XZs(*_q+X_Dz^eX;)zfF3v=q;Qv#U9_cDJO#G&Grn1b@Q750kTXb`-t;66aSZQF zD5w_562lcic>4$z;ZH5Z;v(1cmgx%qZp%I}ch=75r5=sK0-$2=o5g_S#)Oo;bydV2 zz$^Nchxx{qk*#6MCJ##?hV2#0236(xz+%_@bU^jJz;j=9EP53YcoJ(IW%08z2PN*jT z{#hKE<1-~^?VK9k`BoSVfpepPfWFIO{!Wq(e$XK}fRtR|15~+JY^mxy9%}b|5Y1~G zitiFxik2E+z;a;M8$AewC88j0U5=FnEr_360^2ON0t8LE2(q2hzpR%UFX^5A-klbk z@Td69x|k6z58r;Q_V5ZO2V=8H0b5BwME32P=+xw}k4dF=adkXB20_R30 zKU>R<{kLkYfp}Z7WbK+O&A8J}v57Y%BM>YgWrMPKKn=X53+!Zwb*QK*kZ^{5uwFhA{b9cE$k_%1}ninPY88td@y;|-{>j546+Fq6-Kk2xsLtc zyd~=uEZ4gIzzVI^6R`3WKZiY1D#5QoyhExzmGL+t=SzV3_{>WlyD<5)pFqRRWrM09 zDvQjkJYKZrY#W9xKsQBGeh&lC@6d@c|Ipe0ZNiEx-BKTq%j?76@K;qZm9y5Y_j{t# zOd+&D3fPt~vZH~*YOHE|o7j0Lc+x0)iF5b1om8K(Npbo6JBE?A^_wJ#x#)0U1JGnW z+?TxU#~;N)yq^L(!rakArnb#9%6OSNOx5>9roIKD@Mb(;4>l=1T}Ir(uuH0I5^(M6 z^2%-31K}98mgvgx7dpNo!yqD022Cee0D%Y`>vgG}E3qA(auUKK$%JsK+UFTz4tm_1 z^3RRQO^k>j)y`92KguIIX2~Jbg=B)BAy^dSFeooUN32zQr%O3EckrdHS@q)ncTzqD z@Nzy>0n4Y!I`}Rs926YQ0_KRc({-JLmM~k4k6QKlsFL1#LY}04*<$m(U!Qu`HhK@0 zmB|v~YgU40GtH==V0Ygo{vaq{O(GL!CfqPk%2$-F&TXk=yVGfufQ9Ko8VKvirwsaQ z@R0j*+zWTv9D@V{;B>53P`jQM=TspsicgN|DxlTrOZ@viC=thvTn~5;j@zX7f=@^P zK;frc_nUF2G7IU8<*LuU}d zTC;m?w~bXYgF^_qWyK=kM}f1XSb*cJr(9y99OB}-5)>+S_5)kei&1BHp!3gfMhU&j zvqU8+8VDcX7Djl<$2 z3}8(#Ll>*erK#lA@GLLqm<}B#y&JMwbFHSfLj=E#jV#C;3ksvh0*vqH$_rLmsk~@p z{@5eCAUhjX-+HYp)F_-cMZvm*7WxX}fe$j{INYW%WXrhHJam($fx?TC4AnEeBh}dt zUP2@cfiRb-AJ5g`gdmu9w3%|)1n>@e7-$uGYgD{zuBOfw#K8;HuMXdwNlyY^32Fb)n9&v)ieIB(F*oX2F)^JRMP&X#dA#IR5M9`+zk1i z*;3(CVqX1;-ZW|(5_IF!PeRI+7_+Yl7wd18LA@+MT?9AaWJ_|8zMb<@; z2s=xpTs<%6lfO1Ye7{CIL^SASK(2$Kr_1aBSaBi;MQ75or5?_0R;E@^($q|PauK|s zW;l^$NxrmH*wV^j!$D4uwS@(q*3-yPj?FTM-h3{U)cbJ6nd^CyskT+*_tc7y&C zU4rMf!&c}Pc1P(tL_Sr5qre7lZ~+&%W)SmZ&9w=M3hfyBg6pj5MK#2gafvd?3j(1G zS1?QJPJUbE6G1gYc7x)ll?};ps-flKp+xnRCPXX9jaOsPUtkEfk)vobJvYW@`oVD5qBtXK-XAc!jPR{swF7A;`_=UDDVHpK8WbqBUb06>(f_ zoOr~hnX2E9n9UmTsi*l&4!O!>5fSqK7b zl6H!DbuNjJA#n|a%z<>u;;(@H#c*sOmMuXI*_Nf8uF(vk{&N-TD=7>141TTFK zJDlqF!^n}xQ#zEk+G3Y~7EuvfA1^<#v$9JseXQExVY2?)Alt5R5BFjeXDDjUKK)$! zU~9cHTxqad1Y^1Sl_w+c7%1aZNi4UU<}OdrNa5yDp3b=p&pmE0hQfcfR)Rn%F%7Gb zd$zz4^(4Veh>PpV>+#Qnvjt<%LAvzm~F226}QZ$^;nN{h^xfG zkBUpE_!M-;R4Nr>HUg;z?#8pN`n%b^o)rPEBWI0AZ_v973c-5Qu1tsM`h%>3hM?@m zVxpLM?)`}G@1fcc2+Dbd@~a&+KK_*?JCil0&^D!reW^{PJlSSrZuI;Ur-3PHUmL(R zK`{P+{-Z@!Bn6d6bX(u=r}z9ruIify5W|HdT zT6B=i-}V&`m|RPj7YChBF(PIJ3G^=U9ggN)OFw^Dv?QO>0Ll zWrP8z_*tnGJ$(!*J85pve8bb-Q9K*>++`&^s9!Vi=Xp*04W3Pn;#<0_EjQaAYQM2Y z$QS`(mFl|y7LK*k>Vo@y&a!*CkvK$-q;HMQIFEYAT}z`ryp#7wD^+b9;EgUMY+}PV z245M9jZReA-;Dw!kZ<+oFot*(`g^#;&6Dvfv>frbh?m~D7pl5=tGDu$`&qhjxKq7^ zEccq$u9I>59b z(4dY;z)b{IY1t2v?!RDm{$4+^;L^sv=tiM~1xJCgEDg^z9v&&k zAa$bE&_}ogGUKGX%~@%x89&n-xv~TLl7|*Ud+AaATLRNr&5ff{=ZR;d`Vnu&cNJf_ zg#WF2era>zXnTNGGZG5}&fBrz?vIk&*-Jvv24xLT3Rh!RUJAWgNxX;s+#jX^kSwqe z@Z}OFecOn*GpHfhidz)<^V3tlKiQJTGVlYvWHk(u(QYzqMJ@qL`hXNH+-?n$@Wyut zCA&sn_PLaf8&)*(GajJCny=1_F%KyzdG!2J(y^lHKqFOl{EpQ-^wVRDJHTjjnTm-} zmJH7@MSgGm9rPKxof**K(Y5$U*@7jEUbrqcSKp5j7 zn;X{LW|{j5+n0wJA!-Hr57;%IuAjWlB)g4;REK#wwm=TU0`EKB!-+XF*9fApjS-BB zfXdi9-RQ0q31WG3aeyGi|L)+i`R~AvE+AFd_s!*3ViuRAkFC=3zz`w zS+ST8vB8$)MQXL8@_PO`$BjevzT~|@GM0AJL#30UPivBUtad`#!#*E}RdwX~sM-=S z*tbvOwzQ&2EwGsI3i2(WsXbmJP;{4V=OJ5yO{5cYiSkxSxxjP8{1;wNe!IURyP6dh zPkP4Hx)P$5?$Wz9oqrc)pHufa``)1W+6Bgr#@+HO-B%YDm?CJS&MjB@N-#Spv3F;gouo#Y@Ine+yB<{@5ge0cw+eu<)+))XQt-)N5eVzmy|H)7gA_&H6U`0A9jMQ`Py-TViB}mPFv4uEEt5+~ z88P zE~oTkj$M4;GXKP>Ho>k!qsO+il+D2y8%m`s z$Iob}u(gHx(Td#Wd0UGIQrx(Ex&AG9%7&sS!_Jo1qSw6cW5tz`^9r)+`TCW$$3HjD zvnn^4rIrnTb};y$k4h;5q5n+?)YR`7Al*T_tUbz)v;9h*Hw;QmNcw!38%Fi*Ppee! z>(g)Q)&Cd=*tw1y1z`Ox+o)NUQivlSloV3O(J-i1GMVGf zx-562J{BTc5G`-LbNFyN&a>;OJB5QJWCL2ZI2f=J_F*q9r+E1e(nRg^M>%AvH#{n% zZ9S)B`n-72c5k-uM@3euLs3@Z+pxwj_p;@W=d_Uy(y39GtcIStq%s4K8Z_a&e>(Ve z>u8%qS_JG+?E7h7UJ}xg0t_C_M5RF3EaCRTa2L=ue!Ie|jVS_Qsw$)8^UWn(`UXR= zdp6uQOCvrvJvnF>-&e}|ayhMFnSLnQM zB`v^GB%aE%Yi7jQT1A^4qkqbw=;ot?jk;G-MrB;)I`V4X;Su5y1&|Ia+tVfT6~aGh z*Tud`f%QDO>+XOSrkt@Wktt&u?8ox?CXY$mwoRS(fnS!02QR)s=rz6+rAt0kzn?xW z8nb@y&b%&qZS}kz z>xSZDHp*x3OqwcLqgoxma1iF1aL-IDZXN3yLX-rtA*T3BtBg@`ALZ}qbE-yvLhZQg z)N^tVxI`DHwN9SdS((dbps|05c8&4H>^|YLP|u^^JTG3#bp@dGQ7zzxZXNa)|Je-h zr`#GD$-{kTwgYvs4JrZrsxa%+Q;aH$=VcFa3)cE#>+T=@ltF0uJ#pOhqc!L_J?e_Fs4?vx{%G>gSIj(h{N5<4huUfoCdAgKcpI3V$W*GI!@8B} zCAObP3n~mLM2+kN5LOd}ONW(D>=cz>;gt^pwp#OaQ)KWze=`+^e>!Y>Wbx|gf~W`W zU&On)T=NRz->+|7HedWBvZ!;TeMYm^Ey@Fmq=n4p=rKURP9X}&3>zbO*G6>46{ zy1ygS2P}mmvyrX8B6}z-HmBGvJcHcr}__h0lamM z^`z;uqDA^Hw`A-Aa`8A+vE5;Y1e!_6ySMXPk4h3-l|C+inlH_^`t{Q9IJO(pHu<&I z);D(cyyxM`N_xemoB$CA%~!K8|NQRnEH;(jy6oL+5Kj+UCkCw^mRst)@=C!c&_#!` ziOrQqslc{;Yf253GW4M7VEMcLD;;+6^C7jxRnqP=oC&q`mur13(Ctg%o z9>nfjP%dJ+>6kgOP)OiMcFXyjbUW#-!3T`}=H;VuO}JaQq=1x1PV;PYjrUd1N#4;L z>zS^D0}2{>CRgeoqD#bpp?&(LYMW~#?3+Q=>D*fH#{05tU2LMut`T_M=Ut~BOGNc;~GxrFat2-V`^2F;M_xj-}M!BC}VC| z7z=H#%P~`$Z~N`U^s25~l?3783P%Gm`3D>T54i_Pr)m{<`~jPW**agn^b)O8b)Cr*&u~)tDnA;^b zpzi(|?YKp=k3QDC+W5Vy33*;(bVenc+79NDajVU^SB8UpRZNwv9>Mg^F7xn5QNhnq zn3!5a<%WE#3EBW%XxkncY;m44oQzN`rtVw>AOwDE7cakrEN)a$FIe`iQG1Q!ik z9PFdhPqB`TQ6_c;6w=ksJNybW^vnR?4#9k16bqh=zo;|jR!xtJTwG7H6*FPnAw^Dd zi#bc+$@DYz<3}${ZheZ2wYt@gek=VfZehVl=3MxBW50eXJww)DOB z49W|5{C)4j1^r7b&LWesl~MmhtNvttNR<%Lg*gShP`lO*R#Ch6vzX>F@1^!C9@BEDu*w0UMy zQv%C=UOYM3dwU^ZTtO2fXMuRHR9y3G<%1!%nq zpbL82j=jno7V|w@3wX{XY;P!FIAmb42a3 zJNV7KgfBEzjaLc|P&Jv}L~bT4(ydmbE2S1v2GlMeA}i zpv*Cvd>{L#{g&0vyJ%LuH3(p6Hl9Y4d^gsOdmexQVpv7eYXJFSITlTZSc&c-qvnuP(c-T27 zHCT&G=~VP5%+RtSuVbG7>P=ZQs?()*E^9sOQDxyRj>A*9rM2fLen8`Vw zZL-^BR4JRP}>PMc25K1=R6O$>T-$nar&$*SP;bfwXnni83>LG+IvB3u$#0Fu=~ate5b>@2~*bALX^9lIioUC(D&$p6}xbtC6D zN!HupZ|Sq-0wCBcWi3Sx)U%hi|(gShp80n)X| zSd^#}&{odMIFj>WQy@^q7z|Wa=-`ki>BjtI-cYUSz zbWMTsUEgnvdfStpl8$sgaU$wrft)<2!&f}+gQ{t=M;N>6^e|BYyPkf>S%mqEi`E8<^#k-yMIWjluuxdUq#2%<72KayVs)*?Fy6#_8FtHDFV zm_^NT0E8FawZgiQi^}&K_mqguJC!VjPj3nyE8ly)qS)3p+7g~l7QQ~iZE-t$$fN%f zwMN^mZc%C~=@<^Yltq`WfqWd9%1QTWYvtk5T>8d%D~qU*Z%jl4kF@lFVF{m*W|VU5 z#%*{^iewfqis)i6>*zGe088eWOSL~vI!ZuREETHZm-$SBBk)c&%=Lb@I#?I-@mi~z z3tHzXG3}JGS9D6B0MSQJJhVJ|SP|N1U3L9$N(!tuZl|*&HZNK)&sG%)uAy<&oPbaH z8Zj4w@nLRNTs4*yuMYlQ5ua6Sv}8sUgbYkYU_A3p&(C+RZ>@ZEIC!C&3&C!bmFGk- z%NG-p!manwp(5Jfz>ojID02cU%%< zyo}~5w5bG&G+s2)cDz}yoPNoX2rz&O(G;Hf8~b3ckA=fIY4L$Gn+P^h4{0bJa_7uZ(iw(cT>&`|M$(S&)lN_|02) z8No7Wwl}do%TT@y^S4|@tQFbnh6-EJ&IR3#znm@?@_ItMR6!0@c@K4C|F8l zeSbP-+XRbPwY%fZl|2YczgK~RWye>`hF?-`_B`ucA~LYkBa}SqF?3u0QQ@t$DW--z(68>~5OI1^LVR@@=gEYq3wboY$Ir*55r;jrZZH(xm|L0QNSv zOShZJt$O0RYn?IZcJ2>31N~I39DOoV#p;;7ZLRWb7vI&0w{TwBcWYtJ{m1RamX%rg zT&K5Hf8@BO_04{>5O2l&TnGM)5AlNU)7L%nICb&Gl^wg>*E-R-H~T73?$V7+%eTqa zM#XA^xOF*rxCK=7oe1UitMcl%wgRIuT#M%XG)Ew52>Vmv{S7>m&dtzYYNq46^)ydZ zse%*t%bwqq1JUKRiRy+A?%#!5(P8q)#Fq&fVD%x>O+^=j(F8O)pi zf*nB!E|vZIwGlaBT4bjeY#w$W%kh48V8(6i!$X6odBVGldN3<7jJVrj2{Vw&8F5yq z_H|YsGPyEp4UrmtW%+*UPY7u}B4t=!UhiBmTnZa>Iznue+;OlHeC?#3ebE6=NytI) zF_}`tFJ|?W$A+9utrgdeedmc&ar#LcDBrhWK|PYC?AX(5Y(Yx%+%p=D!K;UxS}>`; z!%gzw6JpG~{4~$S#7K2RI>62bNhT}>z$TFp2xrbm=v<@nj@%_BKi^W+o%b_F7C@-5 zEc#AHYm!)#sxD4yIgrQC3qws26_VWst2bO`r!;e zaU@QzmQ9zP0Jo379m&Ml&z>;+<|o0llmXXWe*ZB_3XH=Mu1d!Z5Ne8=C zm)4lM{d}n8?#~aAsf}(1Ss$s8L=}x2jydi`IDH>%S7KJWP~E1wroGx z;6nXP*X>gaH_r~%lszf~!eY{dLu*-OHex~PlD4{u3qzmxpTe}j=6`_{J6QEX`!Uol zK~H$-QghvrMuxC*ytSnOG{Dnbz3I_fm(Zt{?Wer_w%M=mWO-|h6NF@X-1&rlqQBzm z8q6c~`Qa3a83c{2@C?$TPV4Z@g{NQ!7-_c#N12xISiD8ch~ zLZ7Vp2U?xx#T@U#0}QlvCxBsxkG!kexF)#fs>d%JN1D=@02tW`QJ)Z$vG1vKpm)`#8k3=71#gN!l2<3PiN0EBO{TXk4nM?Sy zOEc1L!zXdwOs>wjB$E+FaI?e*PPQaD^uF2jUiV9vfUSp2A;`3z0zDAQE&ETi^e@@T z1{&u?2A9cqi>n^Y(1Pi9%QMHw2zfeTH?QuZe63Rvn3jq}yft_nb+6V$$lbA!-z%JA zD6-PsE6g?(EXDcIuoVbpiY^50yiDBET93I0N34m%g8VD$Y2Wc13@uJ#z|n=c*8WfD z!B`Ci1FkXaK$5Y-Pn~iB?9c9i?b(!yxgmGMb@x(MSQ)g3q$T}+)t54_p$(}{3lp7o z75DJ;5-yg^`2MfUED2hRp#Z2Qr&_B z3JgsqMR$LEaMJdqbAhVna`A1d=Z%e*Ewf+6>r4BIfEMAU*onH4qf>_oyM)r9k_)9;xGP^2Zp%MdRN1+J&X|HdOVrHe2 zoxS>FAp95TA(l&7M^@iiSMuIoE2}L*|15$0<&Sg&rTZ{4A&Ukn?Q^_M(br2u?kKsF z*(rx0VH_)Ic&A0;r;m$eKIC=3e~5CA{#7eD)@0lDe34uC5XbZSm-x)?tDF)?>`mM} zwf6UucaGD)`=#O(1jT5S>2MIMYD_n7!#hix?VaY$K6+Q`awnRZ&Au?@);p8>{wO!obI+!w5%>=<1T!2#SC2W$y{O?H1EnFi1cfs=uk%A@N?#8 zR8gm9`}8MI^>i(}|K(%CmHy*PUh3@8K{n?vLYQg8g@4M7=nTAjZV2#J_{7VR$J`mw zdG2M|NP|*qEOT0&TU^`rl7`)`Mr~4oY#KY>ucE(r^&MafG3sQ2CyIj&6Q_qqXL<8# z^IXQ9;4q98xp&6vdKFYw#Bm1u9&7!CYu{td!heP^P#Dm8I(sSoUjwpY=CXPj5*xOc z>=E4vkGC+R45i<$T98t?r%nGuaIl(+{hPZFKiDxeSX-&o%`V2x^B$?;>;ULmWfJbZ z+sfJ6S}Nn9WhzMn5qNrM7b4L=j|w)kk<79lV`u)ZK-oT!2Sj!yT&n;YJUXoJg2gb! z1|y)=-~TC%?M6%b7T80ONB<3rUShrJcRY1Hn`b|nso&4@Ey)Qqsu0^JNy@UDS7wFw zqbP%u4vyxeiRZZ(unZ*5u5X`-1QpO&=F)UrWv&H?er~{jFKsxU=xau)Uz&kZ!UxzO zpFmiVb`-G3>5Q6=eI#Wt=JAQ-`sq2=XxWyzmK%u}M=YR`Q8t%2fLdQvc2RltAv|mk zCl<<0QM$e)0F=DosJOp_&q~4(qt)>>>1aE1d*%J*rgq{Ci~%?Z0UsZbLoF{Q{k>1L zU!*YVj1l<4jP5n5jG`V{#UAGqk4pI79>U@8rcn-TuYQFP970L+x>B!D0XBy41q>-0F z;Bfoo%uEfUYSsU=;(a&ezGcr{`2}jYv+G7`5pfd!@aSdw%Pl5Ek8OIc@0O*xSEx^D z#BBKNwEr4_mkdGokO~U^xqk;s`lSWSx!++6g14|C{`0MuuF$?>i}s9oFZf9<{` z#PlFvYe1*hd-zS-j2!UR(*;Cze23DkFh9p)J;%76aGL*NF~eiQx@*89cn*AD)~N(TI|kNTUB%Pyz(OQ#Yp?O>6-MIX2SOn z2fK&G&4 zLgBuR7ZkRapTN1n(O>{f94d3%i@2*k$qM^q{sP%))(wDTy?cf<=2U74fF@HOGigxj zj8RJ0flO7$MfcPHrZ%4A9+iL%qci0sYEN;dU4KE6Hw!a3kRH+_WVR65@_!CJ{$7HT zI9~ni`%g^dr&H5 zl!>0j(?81iZ~-G)cz!`NFV=H>y6HUwiqvAZX2z3oAVF@wR zD_gQsH+hZA)Q{Ro#JbaqP?pmPjaLnUB+`Ll3L*UW7b#q{+)}B zntG4uQPBzS;Z;inp10w|1WL`ehoRDZvJz6yY5tef&r* ztisf#XGKw|9R;_o9AI|>C|gEP;h69j%s%hVolOZ0N&P%e(pkxpSifm#xMus39Pc8@ z0oUGS)brD+QKQ)#LclDZpQ7q&NB99Nu=L|n3QHxyj`W{g1Fo5DY%CvF$xyyztNW4s zy}LK))h6HiYToYV)cwxWs`nj}M5187|P+bR% z%iv$|9~J~feD-bCT@_bQNi?#$hRNOSF+rB54m`7P0y}P=Cvv0`$>*>mB#9@!K!L&R zn|6fjIU|saTLP2}>0$mgq&DWzBZ*5+5{jSby8bRH%*d*NSfxBEiqRAPT0$KLB`H&O(T@NE~LXLOEe zyOI-bM2Pt%yR8Zg{2puLU%>;K%(#GTLs#(l(Ej_nz<975+;!yBaaVqrN5|Kifgv`b zKF8DJf}>-J*n~JtS4>>MX6KY5=gc+j`jl|BcPHFAKc^;(__64chTz21h3m58!QzbJ z3FngT@(HNC)N<&>!t60CpfjX@hd~0kRtp|ec(YyUw+U4mCyR;~&hHAKsCRpJc@Gy% zQt!!ducFNyK0W%$g`;s(y8q_|Y$PaCdq5&21GOnb-ck&O zZ&fGy@0=4Z{jij)Mc$OV7ryP%Pa|ebKJ<${I}-~ahNI0KH$C;|-&|26tqWI#G%KQp zRoMJ^jLCn$+7^@;8JgNY9ykUAilJ%6JB&++*W}Z9Komw$WI|da)4i}qYk%a#-OTTz z@a=Z@6!+(=%flujO;*IvF2C%JB@^q+u=^zzTh4@|K9vM_ph6wLml_vnGH5tiyh{k( zLR(5FT-AUXq9G}Af$F1lBLH|K2%ZTSu*V|Uup`EBui4n_QSxuToU5j@25|yUK@SKS zSf?<8no0RZHf+)qoDz2@(o7S>{TXX2sr7*+*?PAt(`TZ$IB?Et2rKBJ|7a^8LI6tk ze!&Q+y20ak=C$&x_M~=?)A3%$hqv(9d2%RCO*@qP<$DDfhW$$ zJMpx=VUrwLTE&;^26}?7MH&RX*h-ii=xMrZ${pTB!zx-8b_3~;CGo;}(}qqPL(dKt z&%nvn>OIh4)4uc0s4$$v7-K>|E+x8GZ8@yH_z&<*@!y%cb{qth{pVl0C$})>D@N@o z%rcs58i+5riz;@A-7HSrROR*;()v_+iK;AGsk*~hUxmwd7SL^vH3+n7~JY)yPOUNTz>k?79gKtSUVjRcEu<(+J$#k1v$` z@#w$6efc|i;b{ko=Nt9Y)90SQ$wcZif>W(u^&GRNKxY|v?{}eeLnUt;=fPt#OSWv< zUp_%{{&2u*qB<7Y_!~cfY5U5zjX`MD?S+!d!7%3Cb)N{-_<(s5n*1jV?o^|`!E@7LaUpTmiS%xN+RrZ1c0|``m|w_sGi0&$>y+`O#R7M zyGQ2PuaDy7ggm8;?|nYe?LJ3mZM~lJMR{npy-BwNwx~^;cmHu}ke~ORx+)(DJ$Z?6 z>+{5<%t^!u~M(miKZRyZWD-_m?(nLb2A`N5M)U; zd#}1HLAI8w6Vafl)ILN8EB@$_pV(WxN)30P-@CJ3n+no~Csz~rXln_;*DD8obN>;? zZZT@r4yJF@JPO9W=^ul+j6?>JTiRU73TLm90D8m~5S>U#p(5P71~#=p*)>M0AxvrFqm;G z>Epm>?GyT=Ff=)93G2Gg?$<^6Vav5T3W5yu&)!8E$=~C#KKVHx%F=cl=^RdeH znTS_&{($%cS@b#|TEDeVBP>k$paWC0%)K*|6UzIncpc#izLrkChtkJUGX%_%mT6?_ z%{~&PbLk~$vKW^q5)Mdn3{>)bBtLjPfBPc$X*_t`K)klu0*F>J+_b$duYoBBAY}%olX=B*=7j?m z-#!MQ<#xAW?q<(ODAz#<5u-9?N{Xj-{dlmx>k%OO)H5p9H*_uZ5C&8|aI7aK0md~M zn34FhJ#!0-wFgye`!B6`N?pHuZEfj6`GppJHPN0wy?CFQX~sjFyw|MLW?Ms zF$%iEw{){~2NTY|VFV_EcRh~g<@umePw1GD->M%C8Y+`|orR^6LZA{Zo3 zc5Kw!NUGy7Kn+YdFXs5cWM*O@WkaGdqR z8^0246)5OVk=A%ngwmBJIed^U-P&{sBrUYl>F!uAyg=L$+IN)PELTo3GS(~5otI08 z@kIp>7jG7WNrn*I_eJL$g3UWZV0*QLFAgZiGlH02d0h9*) z`(UC*XFy6LNY(kNd?i$h%O``#*M8}@oEwt$E18@~&Yv$tSVnIvu}?yLDeO{ALqq^) zFs(#=8E_Ign|C@}`RZuxF-WG#bM#s(nW^V-Kc*_MY|L~~8G`dc6AN&_UI9uwMWAvZ z@Z?MhP?|QhJ!b0rMh9^X-n8~rcu+{Fvb5NcG`a*9+ri&O!27oO%ai+ zzk$6&y4TxY5q^wCDBg#VC5>;@;*z=lg2var3T;EWdsT{wa*!Y!3*vs`r90FSSDN{< z4i+#G=j0cGDI>fj0~y5Py1b=;xk)_yPSU$q;mvchpz;09U+0w5c9b-*feX58Q2&7H z>>Q2C=Co!;?}HybO2IkZ%n08@YH<4!pRlN}3qp@zfPsDk6exk@fq%Qm745gS>;{q% za%r!B)ekNH$sl|?Gs`kPTgx&wf;o%@-tA@HT>Z6O{g3nvF+xh~Gs(D!eL_C9Ht{R( zr`StTJFTx+fn>%$6vXUJ4xx<4)IKpA1o?Vxo#%jj=y$j9ZnbnP_d;Ri@rJ_OwN=U9 z{I+JLSEa#;*d!JS5P7h=VZXhV%usNAwwQDDOQiERMGLn90SGoAYv56PgTpx4V*I$k zU~uae_ha_2$%P6!a^GlF*yb)rE82XUSB<{(!sU7HB}GQpyH(6}pX z=d{p440abaSa;`p1ZzcyF;RhD^-kacQOZFbVZ7TKsr>C}7MJksg;0P_1bBhl?? z79TpJd`=2>0e{f2d-XX3iqwhQg~t6(0}z`q?W+@bAE}lrHW*ZA=i#A^jn5eGUtEQ4 zR(n@aT&AyTH!R|1u{?=4E&^#x1fpK6@&C}9#AZmeL7>c7>WNN`zm}F&#ZC0 zh3aVVEoEZ}7)ZrNZH*pY2W*?djfuk8oBw6J`MF+?wZCpM~i)}vV^OrRb1H^?GBR_oj*kXm z(-=tQ&zFi&U5#YQsWVqLTcGL26~zdc4WeVm>~h%$( zBs`p92D$i4=3byNx-OZ|Vl3_5i8Nx9GSHu;3cV0Q zg@FwLd^FIM$ake49rKas>>t+>qJ4z%=yhSRgg7?8jj6Xz^9_!?H{sGeAHT=>>oYkG z=+b{^t(xM;Y~zezl|FdH{4s;3VE?205(3iX>&m%UmBb|v$btSLkv1p*=)BPF)o%|2 z9l(Sa(W}okddR9&=WQ)pO?kM|%_5(|QRJ-uqze!Y1iN-CtS-)L`1`WI$k*lmBcnBU#I^Ls5!)1KEh8YMho-V0PVC+4m40R1@y0lPExwEm3(5E z-Kx}k0x4eP$Z}TYbBi>|ld31a5D&tHE5G^ciHezC#h@7p{UhXZSO#udbl^$zVfE#* ziYu1cw}%%vz=i1bK6BwDcRQ^jfP5YPH%y77Am5HLO>!;PGk=k;EzIAQXbas-^fL6$ zOKFzMpX_YXME@lox0iBo+W*0SNtc7mlui?XT06IG`Ol9iXQ{|>;cMsQ#sWdLW>(M} z6>DbWF@(ihQW7e~E2e4Q{xEir@1y@V7P@#a0J}JH3NAgTOO2x-wi>iq-{?q0dg59J zp6TxGZo_1cPs|R&+RjYx(4hA@eccnV zz9KCuJDnN59!yg?{VG)H_)6k2hfZext7=oC z9Be=hpA<;Wa`FVPB54m)0>;g+0W&hDbcAVDD^$k2u=vAZ-UvnXXjW<9@{_{7wgkxz zvj3Wq;ugq3)ldTta;a*t8u7)({_~9by5YN*SU@^M-*Gad$tD!|l&%`}mPAI_5wSm# z2iEtJuNov5tkaRsHYOaCfg#4x?b(Ez?0>xXlX(D{gZ=*s(1yv`HDNIwhQVT-rlISA z#4ADdu=>m8VX*(VZ-Q3MnY8p^0r1$`N#H=-@pAelyr~bn=j(3N4JRK(Z@oYh*%*Ak&b_iuJYT$itnGMLnPr_2=lTmWASdA6bOSB2P*-#P zSTpB3L=G$^PAdV50Jn6@AbhL_!j9t~9y02^<}PVw1Ua}+8oXs}8@h$rM(#|5C%r(M z&@k`EJg-vGSh8NACI6j3iZ+mtnn^AP@kxj`IbqJpJ@{dFX^zPRAlY98)X{**cC7y~ zsrl?>d*P?>qSRjtL62w1A{de|0N49l576K~D)|6S8%by4VQGi2C@NkXt3fm#YFAXB zF6&fGP@vPt2Y>K5s@{Wr3*k8=Dz8tJeh7yR^}RY*djy9(*m7vIzjP?%!&bn3IAe-76^{ z+1%9K#abPjQ~0w?rO_yI)vq%D{>S%lTW=rFi@!f0l-7NJ`X$YTwYsvNCzSZHfeu_8 zaOLFaH}X>i0N|6LisIK0+B6U>=p-{|{42Z3rnrHAl$~A`q?RX2FwnlFFTuXyAK;8c=4Ty|>K&2f_->{dI(>xnrbqw2{#02p#$NzE9lCi|SRNiH5AkMDA$&QagdH>k0Y}^sH zYake-Y;E%Z9r6jg1+ag*x-^fgefxr$FX7i6QCu!kKiw`kny|7gxLQJ)klhHpoF!Dw zPWZEKXuH7)XZFW;eKi5O;6z+iiP+V9kJ@R_B5O~oKNIYV(0tCszxSI(sB50f0~Ety zQzpNvy>*_AtpDX>qUce|4_3t>*sy4+!U8@#W(5Gfl-9p=L78DSdB7(-Y}y$SXq`TA zCH*PuUxwuJ-rLD0HPG5}>zCX)Cb36dLERl8LZbor$Cg@goYIkOa92EZhEM@5{oxzr zWq}^_>OqH{728>Cge-gd6Y|=3tl6;l+A}FzHS_9-z{z=<);TTq zL2Aus_68goi$FVyWZDHALhy`3@IJZSY$#wBT+KOqGmQ&0M9aO6MLi9BP^i$N%<~=A zo~L=Yk4_O)PDeu-gBsxV3Zv!+nZrpMB&3H5#y!79+KAq_2kD18-W|ZE$(A&=S*!HEwV|9*y7tK%8d!5KkB}oqg_~@-+kybBK*qk zrF_MS4!uwQKx2TUBX|KaU;wtAIzK_^xg_M6^@L4H^_O-Y0#_!OfswGEYZxTuclO6x zUW>)ZW`KkbadS@dyyVu|^?NE^daG*O>dv4vE7qr#b(&<_}zu`Zr{lxhmOXc41hba&?(J^cKv z?{lV|lRB#Ua5*W54t;#RF>=#fG+JWFi5&hpeuJBDJJ*R6nRc2c*z%?^)<6Si_dW^0 z>k;mA8~W+E{;5!O_}f_^Fj5w3ew>x?Ljf#)08sYGh$pyB1mNP(kfm0#|8Zlu`ot&4 z(Ow*X7v8XV-SS>F>X(WlL^J@6N=E-kQD0K36i~~NNgqGcMHE*R@ZCc}^hOdbN6;w% zFt~3@=AIqy(ZRQsv8#B(HKsh5=IlQ7OA%~GB!M|lQRVN980qLf6ZYZ3IcyMEZ~d5u zF^SZ$^nv6>Dla1lV1|_~i=oEy)Ws7IxMX1OW?Q#Y+x%GdC~*CY_@B6iT*@YEP&W#{ zf-XOSNsa~$I9p8_-6|XsrX0V7ex^(ZeFXrj@LaI~Gx_jv4-T_F?XOR_4asq59|Ps( z^p{qmdTcL8BTfFI$!22On-s?Tj7Mf40!3~u1_Lw_!oAhJN%&I|p>DY74S`V;f_D;0 zdzbqp^FvM?WztZ~@tsTGA(%op!EcK3XgJTOY=1-|xqR{S@ieZp7>DRUs&7oyDd^as zYg7^c@L6*<*36VH1pd@&<0v30G0w{^7wQ>?%LyE1hq_W;pCOyNTAutkXZ= z^K!up7m@J4Hp0vUba91cEsPik_O7PyE+wCXhfopi38nd`_(rIKAjZ&_8o z1X1yIJqv{jClRWm|5nt~J5RCP?CV?~Bg#rLdVyRHvV>HkN4(a$age7%L=D7)N?cC77(2{gB| z#dm+yTLKsJpCAfTA)D7->lhVI7k^WWi_@V8YxLAAApv**y5i*1L3*yOeHL?819Q-1 zkS3TU363!P2;}VYgbF5g?UHVPALa zE zO=xj@m+L(J)?}QzQqF}b@9F6A=|kk28~c!q*#faRsgqm3(Ni$p3oiay9~$)a zRScE0I6iPM`}dWPEHzg9>P;v*EyIP~3&5cR$XtII*_6mgmEMwzKKO=+Qy@dVY(JL5 zZT*rQ!U`t^<&3YL?K zYG;yNlP$QQpFh^6w;b)oBv>O1I>!ViX=w8$wfq|%Lkac4Fdo*Oa{!kG7fzylrD!A9 zXW_FuEh4*k#O!;pr+%S+w~2o<=&3H?b&@;zjTdeDPAVB1Da-FyA=69J6Q4s8rqr#2 zG0$)U4tLSIJ`G|3`uBU$gTq%xLO3Qesrg#?l4lX0dn0YVt_9n{ED?0IIQk`I?*X$~ zXx}Kvv^5VH(1%Xi3SGe;3oHg?FO?k}48XOk+oKM1?Y`AF{ANT~d;mzM^-JNA2Fp$z zIN7^mU>74ryd-(|EA*!_o7hLsBk~3mV*(rEnlZ&_q;hE~eh#cnL)FZP6A`C!}0(wlatlr z{um6nLY$X$@WB%S<2cH(|0~VN)OZ4vc1h~fMbdC@w({kelLjBQmoua@Mkkz;I3RGi zr=61To|S)|@ihD>=&N^XKj#;OO(1M&ht}Axk|3nFR7+w>L?C>aqQ?~TdBCe z%ux3Nofop8RYm_UVdu7I-S#TlwTEe9BhOL_AauBPnvo{#I)t1cfrr*VpI80=GDJ1R zCez{JA**JphB+6M*+*q?-CkG^RT z;jLj^@8E-DtkRDN?vGdpb{aEyAhN#js-emJI}B0l*H5R=F8uuUn}<^Ow7^E<(6M-+ zf-Ed_Y4H)rJrRpf{&3vGc`vixICofW|1ot{GcoZ~pTE9->PtcZu4}~(k@#Oa?Z-{h z8VBtnN>BUFo|*5@XB)ZBvh;KPn3s!eMv!PO-~m`dRj5(!HrL4~A*2g)QiX@wxix=P zy%Vg0r%DBYO1$={QuDEvCf~YEd5|P~3|`M#R$+`+D9dW8_0^kO-Vl6S1Xt7MTyG>V z(Ot_GiVJwj1%26V6(UL5i{pzA<76=CkZN>1B%%?E7MzP*gd1`QP=ZF>KfAiap|W9pr})^}Gj`O5^;5UF}?08xwH5JYv3@zo2$kgtuIIjWhCCu2ycH4sLIH`b4Z}{{2b(iCtxpLOb%`0lp`AhJflF_9Q#x zgI_|}=GdS+6c|zIA6HCZ?TN?{ipn<4uhIH{G`(d&l+X7*d@m{8odSZAN=q#&NC^ni zNOy~L?gAnm!beb0L6DFx0clAU>F!dxJ2sx-`};qy_TB82E>L>16Ep#j$W@bGZzlRSMqMK=BZTAV3>}!+0kZa-kR=;8)k96`s(Do0M$t;W){0 zkxHUf?=4-&df?4jd%wH$l$&JcuF@kaH@tr{z9bOcaNamF9eD0lo-Ln!e>BuYEP>!I zv&yE5&1&RfP679UU4Y!9fKyLePO71Ti{H0-p`tJvXuU9B6A}_NUH2wcM|UFCnt(wBNo^_PGdO8nVN(xes=D*0)zQ zg~6f+K9w>?ZmKHj+|RfrSk$H~j0APxV2p+uBFY6tGJIJA3Z9)ZuYn@Tuyb|@Q+-a1 z9OL+{g?*Gw8Rq7galVRRI`PEYIm1{m?*_OoHo2A zXMtXWi&tbrwBSosyzX6>2%_(T$ZGDYgYw`z;wx{pRobQgZCZW@*VNPUXa4rvgYiZn zrbYZB@w{)s7{}i9;SYs7yIAbbGA#~3bO$?FzGqN96El#!uyt=Qi^e&Z^_(b@U#+7$ zl@UYI6DHQT`bryDxy_HgdA9$UvTYe685VAXK3XLQ+|-=0AAJNeqPfDCNn^^GX+<(& z0%|eyN@q=G5Ptse=5QAX89|jY5qjY@EV-pw~Zu z?2CTHEi`Ix(C6}FJx4GvKCaz zV{KQYGHIQ+MT<|Qbs2NxG^sMl{;How(VLnko7O4KxZ*GZH5hiz4YlAbu8#T41zxnm zdKFt+Z>YkI-mkXTSmHx$?-5@1ckCqO4g#{TdaDc{DCr4nf%;r_i7z&l&Xg=Vn#z`U z*Ks5ALX3?t@293)_975zSi;Wh9h2?&*w|>4)s}6R&LMCzzFN}W>?799xncav<%{#R z;tT#I05pL3o)N}^?SuKF9_vot)QExp7kH!2^Z{nEOW|?JpXB0GZ&UI@I0y0&x~utH zGryss$5MOvWYC$YoA_qV%EMBle_t7768+wNEGL!3;o-8@{*OOk%MeHbf1f=8uP$w5 zHap3(l4Z}O`G~UW9-2?bEDOa<1G5766x|ByFBjiCAa_sO1j~zzU3oC|ssYFLE3pAa;> zl*$lqbgWlwTYZ!NeM~wr@)^(~=c~_!27;;lBZ(lcjAz;vs8&(bE*wal^vm-b4%BD4 zR@Q7zu)fXke_;={2E4qVCy)OBrf{nPcf-4ppf8Si`?JL}+_P|#qekq{@xk8g_lVNtJ+l3YSu{m7IN9KUR@aBM z?(VP|+=$gd?e@XPW?d#L7)CDpmi0;LqJluo^1a0_so&QE#wdri5F*8FD4nxod51Gr zRW(b{7>Gf*WL-X38hznSx@cQnGaK1Y2$aIGQUq_+RclI_DqCa#rNnWt2I^y&?Z zqoTZ^#%;R6C;K{2&CeerhaHWSl=)8z6Xz`rE6$S7$)V6K3t)2au2t>gf?qvKWDoPO z!T@(yyk?=!QGAYqFtzMm`0b?&2ANtvUw}44=u?j>c3;)VF_#00<0$aOBx>j@&3 zfmKpv^2Nj*j{G{K9vjUv3305yog^%uW5#h_eGX>?N3vP-c`Gw%-K0FqTvTs6F8(#y zsOA?NEQzF8#f90sbL%OWyv=1E`=7$KUNNhQXS(huq0Ty<=!pDn zUP=t{MpLbCEnZ-e04FK=n2Zmje~aL0NR)_kbk=|8!|k*CP^{+qOsoP-%S{4V*QgaJ z#*zJeLZ*nE0PC&$l1;;!E3*iGxIL4V_tI*KV!L^VB5$TSOtU18Kl6Wfos-bPgjQqk zJIR%(ukXA{u|ZwJmqeX3elD!-3sO$Dg{7bD7A9I_i@nwXq{wJ{HWtW$B#CzTSIiq@ zmrY@I=A`^20Nnh&jVC5PDHY(l)%BV!$ywoWa%N37grbYw-6)U|yl%Ny-??Hs`JkuK z_{GpV-nkki=U^;^>3(;uCG{~9i7^|_R=A2XcPf?mDcM~39qS2s-jMvtncw=pRpN*H zvA-d@f<*Beh1LF3C=(aibld)%wac`CH<;hT_xR@hVCJ^{n@4bN%HDckW)5x0fP0UK5(mgzu=vDFblPmYo!8QA`25w2=+{BPUm|(F zz2;|u28pch1W1IS?_?j%UE6i^+;NEAyr3Guju6FaS|8OXS3Vw&jwe5b z5B}J_LGiqy@chAM0aQU`sR%A?^arn`d#&BhatiPzHgbxX&V?C`mr@}~k=Vj*iQ(<~ z0}Wt}qes9)NCVa~<6J?B&*|lFzQ;)Kuqh=s(-%eb4sWt!FF@C0lomt)2O;^uG<(9whk8 zO$Yw4tlJPi!0F(kIfus<5=3g_PgbR?inUAO92*C7Jk|XscO?z*mtF|KAo(1XFwT$S z44(EFum4OONx#O7l?Y4k;bbk5c@NrOAi!z9T8%P0#SWJ7)cF)l>YSw`P1sbANGs3p zK-+k#d<8*4%um3Qes{@}x!*+>{sy2UBRrIwK0!4#woyxR?_QD!RF7$!$n;glU|d};%(!n*TkfUq>6Wego!rihv*i^)hwjEwYT+) z)k}WZZM?e$z0xN9D4x~jz=3o=ba0p=O^z-qLVNr+hq2-NVK?*`C0dBGCs_e&5ToV4 zb|0uS1>acZ5Xgg-UN*qqS#~;Ghk6#)D5Y)>z39Jn;5Cp}D$8X+NCv)8{4*VsNq!@ojT7Kjm^-Jy(F9KzcNpFlrsT$_ophN$D(;Cww-_rG0+68MB-QQoo`9;_9glcSukYjtD-rW;d{4TJKDVnMdA#=VB<(&f9 zWh>1%Ht}4eC<0zj_`nn{QVVtk>18fx+K4G~6%iYJ&}RTT#_3=QvD}Ld`LYrOEm|H1 zcq!jIw&TluT;K&Uk>CTtQk*{3grvAj%)0yMA_!e7IqMW}j%Ov^@d*>JZaD&N>P3GR zp88qvU%QZ44~wAKK5S8ZjJ=G+{p26cLcfPzEOhrNIqx2QkJA`t3D)||5+88 zy^!()huF7Zi9o~Cn~Aw{v@=6j_2R*|3W8RyT5n6X+LI~5+*ezrIDbIRs$x3Ekw_vDB~WKbs@D$KE9p^#c6te z%C&BUa^3L=jan0rS$ACmXt5HNrIi`yDb8?PH&Q%r+x` zdY-jjny;M0W$Xg%Z^a~N!wRRL5Oide$vvGguPWF=1eR|9>th2ataG9aWmX+HvzUsw zh5k3^oS{^jV5L*Pa8H4(?GM98=B$5f2`1mreIspg9@lT6A278Ta$KKUm+;@wC%O!K zZ6;32B%47HfJvC1>v)oi>#yPuEZ11(MAxuP-0&FfYT=7!BxUJkv{Bq-*0x4S!oG~G z2td;LU#C$#Q4B4=&4Gzf01IceidcFX%eW~l{vl>q)ZigoSKe^TRD~ zZ7KVhm=xxZ#|gwE+*!b&PTb--_4lh5YYW@kk2iID&R@t8hy=?hvl2?P1dAa1u>F~m zT>KNg2xAaxtw+7$%o&k%!dbWen33f3qy%l1?>OceVZm!Ji*Q7|8wWUaHb zPBa~Qy+vN#+cg9qNrJM|#c9*X(i*E<_>lJI**8xvi0bWzi?*gps_yk2K8&7gZ6eUr zckAe`GGfX6u|#25{_;rdS|i;5>H3|d$ql;{v=A%ILn2eA?CbGPV6&2|5AWnBmLE__ z>s{D`iWa@Ba-98Ntn*D&I!yi_N-l%xrc7JGqZX$K##dZca7f*I%y@oYK=9mT?yaoR zE740L5G}7bUe$az({bwu`lh9>QzwbN?b)rb65>Y*_oB6!l3;(x5xDaqUx7#rH64`8 z&!Wo}(y-5NS0=dGck4!ZF2M(#g)Yw*zP~!z(Z4T;qj(*#NN%jRThTwuzpx0ykiYO? zU{sE5ALoOgX+Q4gMbZLdg+H&=aLYuhfH1Tlgtdxb3pRnE5qH-k-jf`zs4_3NFpm2a z;0rD$UM+3@1>Cg7*^dBVrpy{Us!&Z-35L_ zVT7jZ`C|sOyxFhQTgq%9=QkPK81GoCiHiii5oeeWOb4rX(NN$Hz{#4@Ai)#ubNuGhI3i)#a^^Ct zMeltlmUf&FYr|9csPytaQV{X^*;)hwak_Uu&9fg5!G88us$c*_3Y`Mb07f9Se%G~H zoDw^J`m4}=0E+u6wdH9@z2?5cWc|)+&W7_XbNkdBuif|f+kk|Sqb^D|{0os={&0OO zCuX#@96~CQV(z>%Y#G37E7^a`+9G(Cxo{28#fwpu_tG!}1mHFuD+^Z@`YXSSe+e7< zLU0-K+&Sdeb0&q|#_YKBH|w;(BH#4Mo=b2uGi}^94@Th9#>D5ejF*;oXi;Q9)R8}& zpz41YE+a+c^P^NlYDZz%qiq&3v2%q7KVse|$|a#G+o%RjI*B*yN3h?Tt6EG>H&+33 z_K))rjOGu?FB29z*>})Z7WbymS-;0Bjw8dD#gS9(l}IIT=E-x9S`=GQ_ftpV?gQ?D zE_&6YH_DP9aVmU0)f&tcWl}9t)Vjn!%7B+l6obj3U0fPNcu#U7G!QG%>x@-39 z`M*?DY*7m*XXSUqWr2~;QzKEM;2uz@ID}p}1Q|;ih-?@*JX4wm))R2e%Yyd5R;ER=`R)B*9~fGQQC+>)kg_N-4_n>C7ajAk z*Nan)n&I?|WBs0rPX5S)^=rWQpg`UU&uEit4aA?83OR72cifwxNeWXx{6}Z4aCKVW zcqN-(f+~=%e`qjZ;;#NT)ic*)(=^1LlWVhq-QJkD_hEd?ofz`dNyiJzcEOIMsHqSW ziYR}P@XL2UeWDIbc*^?PtQFVxJMmiIyOZG7QJ45RL&-d z&XO-7&K;io!>P_|A?f@kJ{HXF*feUaFLAL5q@L?0*M5``qBS6Rxok<@@8at?Usemx9I(%z(%f5uY<_IjwG0 zR~{z+i)Z&ynqd^w_o-i;Hk}K;SV@;VQ&YkswB@#U^^7jzsV!(Uz#d2aNw8N+n?W!L zms1C@1kQ#=v1Er`N9MHME^HtG4hZT=|H@Bk%$}Hg5SKi`++LnkH9vjm3ymlUmid}%z!tgin6Ua+2KQThth)} z^L5Tr*Wnp(LMz8mY0DL)y*ws$RNeL^6|Wf--gYmrRxmBP9u90AY2%8##E4O$$wPy0 z51| z>xH*FDcUL|{R~5H-hDkM@>S23K)TV~pWc7Va2B5{XJK^!ef8<4c;b9J6Pqvn|I_8pN4ubJ4uKGLBF-AR8RM z-tkc`EDL2S8P1iHB!9&S_A82Q>1V&ICnFK=y3M8ik^Zs{5=-0$0Y>9U51gOpd^|vn z=p0bBaNxhd7nuCsnyre=Pw3M6^!nbRlV5zwgxXPjALKBz#AI=b%gHs+%6ksTRLop4 zDh>}mtpPXbeZKHv(l?8Ed>ytmdcsRzA&Ff>K|gEip_jcl5I+l8Tft&CXhqV4G-maE7J2(3qW0d9hJ#A{9}bwR^KS2-<2W{BHG|{X zyf~w+(n}ntjzu`@H3E^I$uTvp^WJeu|2O|ah01Kue(v|+_?v$QpFz&ABQ8sQ&4c+J z7=ed9-^)is{*wPWVk$NkwvWnp%F)0Brep(=nB>BDHF8OnH{8bZCU8HQ%#Hi9OmDm( z{$J2a{JqC_PknlV3Wlou_}r^W7OB}Tb4Ti51%P|@?XNx3t5E1#QpkG|Q?$vBE$53_ znppdVr7P*7_n#vco_NqY`BvWH910HDy+eRK3jc@oPU}?oOKY+7{0WC+BEIoW zBU7BfVqw*3Z>p!6KM>clw^A11XKc}mIpEd+6U^$eay}`E_7mf-lp%m3i&5CStv@IXIo_{GEEqfwp}R1Ye*VUGTi8 zYZ@6c7=Dd*y9pC-tP1ANHbx3>M6X*{aoFv$9-JNwo@LHv$kb`+OUVTx* zd`*K)2I;CjB#%Qx7hzugf)*JHq4hfdFRHUF;<~VyGcq+O)mXmB9!f0uf#EONC`o1Q zZa6la5L$P=Smfk9UVH3x+I%j!pKAg0Xf{1nk6!0tCBU+tNx|QB=2ibB{E+jQHqvrI z9lnWMYH+YL;FmXHiaR~1R_#q34j}8_)jlC(BeNQYZVKyQY}4umRBNu1Moy*CNesz& znO`W-Ku6Z1Op6O%a-2hgmiur7YU>oVtPL{#WBD{OtC9=jIejqK!3YZYTv0uXa84lN zK__EmS9x2eL(Q%h6%5}UvMEWGQ|!Y9kK`dzSYM(vmRjT)1$Yp1dg5C`s ziF@Z++S@2?2V?8bd=tlxuFIfrSKZ3kUFA*D@t{GaoES6{pPNQHT6ZnS!A^GxWM;}WzGBCjMs(t4_c zA9>LMb_pVKSr)VCf@-11)TT@w__DkP94L;KCq9N)3Q+gMJ>fG)^nZ0f-iA_BY_(rG zSfeZPZgIL9g>w-a$4^Zuy|cvWZ4;*|=Lxt6+g5gz?u8X?QrxiZcqiCY;o62IJ!u!U zr{ZC${V?UonjoJ1D^ioM?_GWt?O3N}|%6;PTL5c&W9Y4_&;= zf&SP2?`xk%h7K4Ntw{Ai0+smXN=>gB$qs3f*jxJRTYe!++uklMwJbdEfsRa^Q zaQnd`;Q$Z|LvY|ul+F?wT{gGhxATQh3K$sB=@+a&w;#0tJ{@t6vx@0-RY2`m|C3B& z6!AKnA>KE+^CXEP0+|e)wswXL%MzS-7Ef>}HQ9qiY8Mys{RV@Vcj1~5qj~-6*w103 z!Tb?{`h#22qYTF}etYQ%g?PlPugHgYVNYvHYlFVD#0vO$YUn5rGp=7*27i|=$YmMA zv!QE4SyLk&U!qRdkmYc*Q~tsyV4q*#OmmnQTV>Pz^R^${!S7vEZ$og}oV3e0b&G1<^4X1IdgdZgj`qg~pvFxra= zTXyBI9Tg<9a7?t{HWTx+_+g!#l>!FKV>%7+lZzh7r_hkNv{bIyRwIm$tw(h6;=_mm zR0u06$s3y<+(U0>z68OAE?wM=Th=Rg7o(!It??mE1#tON0#c!952S+yUQ6#BUrzg^ z%icx7(ZqZzP_e&3r%wj&XBo9ygGKy2wD5I(@zhbb2^-}F>o2vvxjY6C?e(`z-EKqS z^l)SC=u`u%^okveBsyCJkeaY)K*;zkOdqmsIqLFMhWtK9@5%?2P6Z%bE>$|u%rbbL zGFEj8QjMZ;_VTEVXxfIj$rHpbB3O#1Yw|bjM0|w7*=PH*RJDZmyP>x=7kMgLDMsBW z?Wvt9~+b|^(B%xH@HE**c(XEB`enn)MA^Fw6>+X+1K#0q~$vzf}@2k{6B*lhq)GUY%HG1uiOt|+Idt$g*$X|DG0WpWro~E#H>K+_S@@} zE3vJZ@W7|J!9EQ)P8VAZk~wwyu`uCe)%;}6)=DBy&Gd2(fb{90_d1J*d7eHudi7%5 z7e=yh%+I+No^32te?%A<4b`ZRs*PE*`Lo(qvU0rMC7yqiFh_@F5c-4_p{9ZGnH*{* zMXxGP){0+J_Y9GgIf-a|F)8L^C&w#mC&|iB)Q1^mJLDB9sU%+F1fN8q+SHGAjM_ZL z^aL*o79!R3Gfdt;!&5|UT|FX7Hx%23eeBPuF>!N~%(N?eQ3fQ5k}c}gS|4(pEnsJR z)10x)Ib>v`x9M{d)A#c)6_lZ1fWLpC?J3BK<~_ydymZ|5Ic&G6`kNIhCF|aErjub{ zwdmHK6-&{nX8j$qZ62KxM+!CRo*rI%-uS=D1HZ9DnZ9c$b$MR=2nGuNstQWZ-lFK0 zClAwG$y4SV3t0t%zAM%B`SJp4;stvZ9M|VONqCeYcYQAkC~U%7Z%xPK8;d&oDzGjP z@tcrYD__z}j`y!?G<;6uyzLix6hVY+FSKhJbaq=m+r0{!ZMw|woFlz5=3@Li5%jbd zt19@cEiNm6czob!hK?7u9$}2E#_ap+ar#w8cmKLKbFvo18^WmZ=`bWUf6D()0L~VS zQno;P-FodE*+ayQ%#Xbj#pi?hp#`c!3(KR9fe)Rr9f_q$_Pnf%Zjxm9ghd+FjQx}3xxI)CEHix|L=jyl>B zzW=6wqr*#wsu^+VfWlaIEPiV-SxOeAkr~K!X*e5A;fEIZ+~&ZnzW59u6g7=jQSbv6 zee9c|vNeVoxyA52_NU9o>O%p)AMI^fB>BULXQ$nbi?P=d7!W4nX2=vuDc+c0v z;T;p2?f{o14>uxHJZkg3beq89sN@g3Rh)H8m4WDUj%<2S`t2Hah-vYmS;=;zd(D?Z zGanY|i1eDT?Y9d-LWow$XB1aaN|H03`OK+co|@w=yNymxc;;mA>cWs~-uLX=D(;)RgKr<} z`%)NGpPfM2;Xr~bhG>xyb^khFTGV9zdYD_*9I_StenI@V2;$(D$eZ=V%A1^Xf<%Se|n7+ ztZks7-{n`Uui-(VBm9szygm{?sKNk01@A?`zQ{IU{0$?6ZTK2Lt8vKsTM1>j3!W9o zc2y$8y_6)F?8Bep&yzey0xJ}H)je1$BA>DP11_aV0`kTW5)+|GdhAA5nO9w4gP_FR z)(P@YF7;@4>OFwGcIR;2BP~+roqbq-FYUP zA>kzIj^k)EuyX@CJa5fhn40QGRAQeweH*tE7fK)oRXCMje0(37mWMv0Jkf8ri)2y% zFvad z%_(s((7?piN;(&C5RJSsyYwWOi}}Z5zY2t@9m5s35{lq$3I+ygo`Lre@HvI3xPWBw z&Gg5SmN?l)Rh~}I%;El~WtKv%K_nz3ifQ<4PPLzaz?na{n-a(|w!eiL zKv98VP*PwZkeIfZTIc6wQMSK+EN^cil8^tsxhzunv;d!RPHS%e4)BbX(h=gzm@_V6 zan<;Z;I&N&K~`DvdPO0aC4%Wr2O>iUjAJ@$*Wt3K#iG8&{e$`(R9 zYqYhlkxLK7hsA^cD?hA zTj}QK%jXWV?E7;Fc0)qSkasI5o{__m0|^M;h6!*)M_hDcH)?NY^vJw@${es=O(V2< zC|MEHoBI))+c*5uP=@0gS}z%q?39#$zs!Y?(M&WqzTpXXHoM2V{`6rC&f3vJ1uD`W zOz?WDlbA?28Ra@-btSy!GD28kh8f6Wvk)LfeN`1i7X(nnK4bBbtZ`m5XWwBp{n_d_ zE2*`b^ofNK9*-mj!GO<7b>pU(0*xACqrGEc&xc-Oi1gz%&>w zu&Ou`nS%yfbV&(f~2sz?stkrp6*p;UgGI~5GY`GNOM~whDygH^Mx`ggzn-t^b-HO)O!oT{|bX!|+Ya(II*9&E&=S$k%zmQU) z#=8ycD^@XZO@1K4&#Ty>%X!AO$E`(#38*McVo7PK>E~meR&jn{=s<{!uV#9O*TiAQ zXshIND&nL{sYsYrUZ&Kohm zU1Cgnac}4zfKtkoq(`mskR&r0Ez26hDZ@PXSX@o8u<3<+aJ7q%w;nPVpYJvPSQ+8zptn$sR%F5 z>O#l#^%j2Ha16=mS^uApe`t>1#&jkkkTojd?OoxearXwLVb1{3twFtjgAr8K5RzWe zQ=C(id**$uIlUbF3kSjdx)ntRg&tc@P572)#~CccFP9j}^SBFN^u^wmpMlv>!nbFE zwR!#Bc}F)C@BQEBO!6RogC1ERPBP0z2Cy!J#eW$>i*gST%zx8^c^MN(crq*`nfEIR zkq!#i0Vst}xht^RAh#H#qy_>`eRVdDsy>(sCBT3$`-VN#234O!c$DSIic!LBUH2tn zgJ3}9p8DFh)2|5Z6Gk+{YhV!L1)%A01F_akKkw4Y*Zv~oJYicD!#ak4bb?3+h@l)3 zI9|Vedv%NW+OZxHU z1S93(v8)WUrNdO~<)bv*)~t^^h$WHt+qqNLWpO?9s;;FCi zFb-div1Q&4{l19=lFjG6-}u%FjtXL!v}!RcMj_|ymFgQu-nXc=rFE$|>G}0vG3OK& zk%os?TTx@fa>QmJ2T%NR6t7~;`S%(7MbIL)<<3x{)tYA{#>C3rv~f!d`Hw{d*MfyNRq{jQgt_VumMI}sZ}UkHUC9GNh;Z`<6`5$ zL$(KJf7VfAxY^79`=S>5bm0eOtj}SgBCR9%fdMFw5ok=mX~2Tk>1#1hh8%-ALBIST z%P{-iNUxPxNOn_HTQ<^Q6HG|gz&Ozx4`%=#e+8s;!lK0K`_aQ_KGNdA+zC%98M+52 z{B>xOZea4eznAuDnSSy=4#j#jhIMeM(HJ&?0^32w>ebtfpVE1d zJ1Fep{gy;0Nj5f9?i`A=LY6M+h%@x=n-#=*`SDh=uW8McEqgWyI@5MOh7yaQ@l4FO zQeQhVm&q+M+s`Hw^FtaBa3ZU4Ua!|BF!P77_+Qs=fr0T7+G@zvXC;3>PuZ561(6M` zykXBt;$m}qVZVo7hK%HEvkfoz{&wS_@ssJRQAy=^dQc8GCSJ;vhXTYvE+h5N=sgNgZl#U-VOc&Z^&TRa)A z){ zrg$dHZz|-ql~U-cu+p%^Rp;A2gMyQF3Q-%fjaAgX$jN z-=(SNC9L=r6NR@VqwNzCE*snO5lS3t83<(^yrIveM~-WNfn4tA@$uKxhKtoQeI+lF z8bxr?kCn$#AU7~)ICp(7KCxMllp(}GBRj_MHCBlu0bgbO#jg}--1vi|!Q_dn!#@W* z!y#X6jD26>y&Vz|S#r!U`nhpWu4&>1f8gD1_49r;Gi+SL>cN|Mmi%hsY>*|QeS8>a zieJk6!imPk#f5QuwjHyStCKB!thlLe#xpc3ok9BIKmg!}_1!9V247Ok>0$^Zv#Zw& zSKyf#s<}@*YL*EDUH8XaZx^5FXHC;0OGhqpHR;@6(E}xj^Y!73+s`xd`7|v!bck4t z3jaZ=_Vnm8$$$8Fl{h|1m@nHx84}gH_!XEJQv#d~SN}KBW`{^3RXmn*>#6g698dqA z9`YZmvh-V`8^)9Z%Kx92Z}oWMo4achk0JBf#;@(7)4cqRhvDW1%D4JPT{=m!)+ML3 zzT!XG7S*c$J4=c;FdzrYb)t8LHKb>z5L^wK)Q)%_A<#{$IgX!gr9ApmwLLQFLYPXH zUMvYfybrv+)rx;!Xv)3ZuYJA|7{x=Sw`KCSvrC85YLgCBwRq3C791a|v%jdAeP`ck zLy^`TE^G4-`(gA!b!RuaZ~7H^PJ8r3r}a&unygc{_M*)Sz5;E-iaGh*;aa@5b)x@^ zMu*2|niRUaLYe4W#zCBe{bQy^g=DuhxVkDR>WP!RL0v|v(#U!{Mc9CxG&+ptO=VG9 z)FJ2P=6`9-@KeR>(+Uzi&pPG4uau|Dm#kMjPB}*?y3Q9^t#cyP*jN>&+%)=1g+>nV zf6opCn4$j#m>q=OE_*++KfS*j7SidoO=S@L`vb#Jk3HGQw(Dw_2uP9iz3$<7H;>o0 z2G_qnv&22jJ1}I3?O;Pz^2VSe)Qjg&`shBD&}M>iLFP=l(BqfmH!A+V?I#1eo6Gt> zGXM8QpQZnP?FS?C|5!en;usD&-=fl7Y?4;Hj|pG5k=^Y7!vpn&JGHzvj(Jqw)_(=R zvIa#u8NNAA>{yl0sa7(h8O1lkKLa1D=cBoN3}5T)X!TaW`2uT~h91NV#ftH}=*;aO zt}Wmd;j(>eIkv*g;63_9+kb%~YC4YDZM*8hHDQt%s>v9~LpJY%fR&Tqe@}UH8WlfG zGb$7UcS%-U_?aFKclq=`5TFDYbf_Z)eOmYnzankHT98|r<6tlx@xYc$ zRQ);kP>KJ_Wsn+_dF%IBe_qIr|l-$i|JVs^R3x`WN7(i<}r^J~CT*4CQ;LR7mu zMQ0@RYf35Z0&8nGq|M7gXq=LW)P0%w7+sYUY&5fN|Ci z#SGaygIKTeX>Is5M7-v8N}Fx};fo9S}lP=o5>4T5NaO0&K*{lcFN zq=A>k(;_2pv~XNa-hZWof2Z0)>5McLQaWP?v(JRUm^eD_nnSEyTlpj#!=2Z%DJMnb z7arpSWopIvSquLcN(?u`RiwHhDT$x;{EgOHDQY29pFuY%YydCXRu28}{UujJlpvAi z+*sCy>$P~iXl)fWql19-nShab{Oas8(Ia>0~K& z{1C`rMFOh!dsXrJU-&Fe;E9qmNt_HLxr9E2ZAV1agsS+~Cp6|<7VO#Yx^7t9a$j4I zycl{RJo+FvmPO4_3;QD$WB#uoV868tkobYjKk95$0dxcrs|f;9POWokDEWwtlcTNU?sg0gK+vYO-12BGP*-F%%$sfuUpYB}{YmgL1dt^^G1#HfQN$0p4%6Wv zZ{G@Q0r{`HtsJnp6_wf*vw*|l95;-BA2trS^lQ`UAEUzSWJ`&U|B*mk)dz38@->}O!RSC8$M#VM9U?)Z7hVuZo?T-skBnM8XozQe zo{11U^KA5Ks^e?N&1ViVNdIJ_O|BdCqkPF$!^tObg>*J|jM%vdD9XZTPOnq!cm43A z`~_W56e!_9PZ}OZ-kwdj#LC{5mcZOm^j!L1I9Y?DlPI5i*`Ea4jjEByw^<#E3>X1p&wgacXGvj3QFFH zt>=W|YcdNhuxVvuORt%?aGaN~Dh;0gxO*I4KX4DJbPMDWqa81&jIkn3ZHrlj!Lg$_ z*gpcnp^tc8uupDnarYJn=Cz6uquvH#Bd^0FT-HZId6AwJ$kcDj%_j!2kzMQr3aPPi zhkW?Vol=d)-!*P!1c7`fm1cRVz{=u6q7RkMud1cS{pH-j7!4(xZ_QW&{@!E(U^{yX zD)DzQ3^BmvGTMhL;3p%{bc?=?f!juObp-n_qUNwWjEXdG@Z=1 zk5@(}LYza(*sd?|*^&K9MF%gdiUJz?a(|l~TF? zw5Es;73s-`D=z67vd|G#ma`ZsRTH^Mx>f(D3B_FpSQ3TT>9-l%Tog-JDyqXHm9r=M zIBtk3XTVkbab7#71h4h1RDXe=Q?q=aqSE9CN-$MjTsihOwrU*lO`TIGqg^>fS+~c2 zt?C23bwg5W;E2}y0Z@bST_;hQSB(NMTM^PhB(k47-;94KOn(w}<1RC;suIn=gsl~= zVTM*Hqrr$QGR6HV zm2XG(QjdH@lqjA0NC>q4I}(ZJ%?z0gMtOvB$Pt#nbx)n6qgN+%bkl4w(Y)sDE7CEe zCNP3!)oJ_iGrvMk8 z915#{`KO@q#h(uAeyMW2#E*YqH&C_B_~kP0Y28|mUDv^*Js0PjMuCczt$aP{ z&VgbYO3#%A9`JYQQK}by)t--xQ_$HAk6C|Y+~Xu0blJ{upEvm@Xzo2rk+`ZpC{Yqu z4j$GF5q$HQ*|ZVPa3?L8J(8R!p8d~CE~2W7;5rB9T#;xo;b z$JOV})d87 z1r4LblIpJVZx;Al+uUnPCcoa;B!AS0SC(Uy49~}KVpD-*o&R&_6(mXcrO|1c8Ul#j zk7nSCrVlB&%SA(x?zFA@K=cnH{dde4VJWSfxELP9uyMik=o#Dl{2Ad?CEYE;(%nlVT}s!25=u&UNOyPaK36~A-!rfK_dRFM zoSK<4@6%CSG7ZPCKE$@DOUPdsB?w0G1FriK2NP@|wsRG91#!W{NH#f3J&6tT{g8&7$sujB?+H~ZA^Ssk(?XW~cSNdF_rIBg zHfH3d&c{|1(SQeH$D`{Yvo9$ecr3B<-{=q%+(;5UD8NN_ea>0}C==(|;?DY7gm?jB zbomk9=MMEmM&!62LI0eT&}F4=GN~L0sVFv(m8i@+jPVUsfG|Jo_lESbc?1hNozZl? zH#nf|qW<0V_Un!?Odj)fZP@L5ql?3%Oy)#O{_4OY3}yqh*RK*-6Qm&GmY=z=mtAwL zj!81fHp8=KQ@60gS8)7$ao^6#X1CPmY>RAr_a%`p&qpec*ln)2V_$I9e4F^DkJHt6 zhyKy#H|;Rc23rb96VQOl%8Si1wku;8wvq}ngOa!BhW^bgDhH=n>@TWuZ2iX+lOk3>8nCFaQDclg9q^?6g76A0@3dFV;UeJS za!}kRTS3~q9JT<_0GUr_K=?UYy6q#iE5fvC(_?nSgSR55EH(*(r~wi;Lo{OHy!P*# zCbXL+?`tK-CcaPeGUwvf06;R*=1l0aagTzsXn140eQXHn64{*<$9|b@-buRa-Z3x^ z_QwEA1@?e?wi=PIMau2Mub`iSGBawj_Qq|4yPhB0mwTJ6+%Yz8XDDILk?9oY%v3oT zpaRYpp6%~j><%!yY(b>_oXzWML_X}Nz9z44ts0(;G5?Vwan?`T?(SkB1;dA1S{*ly zzFiJi>$TR~rkdy`S|+t2etbvmis#ia&8+ec4IhM2H~7w%VUY4o8B2I;M0}?Ui3iFs z#?pnw@J=oR!LP;CE^Q-@fX75T9S<`=rFszF7&CPM3wjptB+dh=zCrrF<>>=C+lV-^ zqrH>q<&w!vHwMtb@iX9bs11xiSEn4$Og9dNZc|P{LyZpQS0)o^qH;|?vHKNh&>RKz zss(4`NgRi@74cxN?F+Pt@chzpg>pN)JRjxrc3?N-JQ932iv!$tyF<|B5{=JT=}mmAQGX)qO-xTd5nD(MpvMnkMhj9sU^Z%1hKNs(Y_# z6)Kn7@Hy^>ej}=LWtr*wVB6#KmSPa0fhFwZ_{SVkz88k47PdB`caZwHcCCx`F%#Ou z5X9&_eDTM5CO0A=R(w$*x>Uym-h(YRiB%?;^}%Po@5Q@Zlu*PUbHGs1Y1y=c!gj?c z4-fQj6Fj`dm6&@nzGr%g$>7^X>+F7?Ei0c?edX@MES>iTAROF3UUGwwg|nUd}Bqg8G~ci3@Peo_~r!Ld?2IABB3L@RYOr@fmpMKLP_n2F9WCnEW@p z(kMHW$W|QTCtaOy)(cisB4@mw0`DH&24&6J*jT#?E&l91t@}a|ZHvdlFEiL>Le+^m zSP~u)uy!PmRL9?yAv48NaPI&!xqKQCupG4lh9A6T@Qv6>doo8o@Elxbt!yVTYCcbB zOXltzqi1A4oYheNm%?}Q#x1p|!LYOW9eOT)Fag*YCp6EJksyppehoLs4>R4Xa%~E4 z?1@k{dITg${MLS|BLCjz8a1b6i{#|AmmyI4B0%@o{4@iy)8wS|UE-^UvF3AOsI+5! z4e``lUUCqJCZLuE_~VB9?#(>xH~mHj_d{LEwm_4ZeQ=B@2z`mm91X}xeIo*K1CL;o zP*_X{lGkff5hL+n;Y#01ePUtGGS|h8xQHDci`In&XJkwcxJjfVe*Bf4*U1QM~(pSHAEQ=Na~XWWhU)niTejcgx5}JLu1MHQP05gpl6T zcRBNdvQWpFOwqc$AS)*CTf}p9XnbIj2oL_ccp6lVCWtNdbP=!rN3NeLNUzsiL*dq@ z$fl(Lio(x6gEk*H5>+n8SG#3slv%OgX&cuK`%2^yA1RJ@y8?|6^I!g1jJ2>PWe z*&>zUx<{(gl)Bs6WH#j=X)-`bNgAC-AFPSOEq9|E}~YE&(u)q%7Yj z!HS7q=b$%wK(m?gLa*}kuqUG@45QBE%YKB5#Q_ia?_K;}Q+aH{DNWYuf}B4Bg=ubI zduI$6Pvc_X5fNe>rwej;HzMg?JtU1?)CAlgttl@Ec3(~yn(z+lt{oxb|05L{xlQvUE$xvNA1fUAP*PvuPEnf>Bq(Q1xzwMZDUy_-=lkP$n2 z+mA|9ArP@6g*oSecb_RpSfjG9J@um2A&n&xT+4w-w>O+OJuyLI0DT^@RziO5Z=nb4 zxYN3@+WxL`4PdZHa}pyFPbyOH9M_i{Gvh+-J+HwL{mAK01iasKh3^Ca)e6$rQ8)#m zbU{z!g%3$mNWe-(qDwoAT-K=xY7-n793KE@lsET@N(H%D4gR6r;*YJ+m|Q+}Qsu{| z`>5j%IvpxBq&NpSAGx3#tK#U1R-{@rApsfdBk*SrkewXN$3r?5{_{6>Y*R|y+=ATs zhbU7JA8e%+;70WKW;1v|Fz5$R5QMfy2ReU!(vu-w{X)>vh1LPXVwF6k zG00NSGw4GM>gtj3NCDMazL`JHu;Kw$sXbFW(f2VcEJzK#QI6(Q6u|GF*PA^j56r*H@awO3)jf0F4IbC z8h9O1KUe8hRRSR1O+{dVE+Hd4)oa2#%{DRN9|kI(-4V)H#m=%$L>+%WW2Pzaje`V@ z^;5+h5yoE77MOjT>rU-CLiAZzJ+rE0*jtQN_-f_g^JZP{;>Np=Y8vqk2QaqQ%C6P) zLA;Odt%Y)A=uIxY*-2cpI6vu+9xx3HPL`DE@Z+>PjA=+slu)Cqla^6}%KOy=Y08y$ z+;G=MzDOP6LrWQvbF)H0g6W5QYeT`ARyqzO1YzZAKJ$Q7f{Tl_n)EvZ3xHmys;YLF z_g!YUNNIY+6%^R=?Pk&fPl29uApq0ocNk*Gz`ojjZ3bRrBDS|T+LaqoPU9=StYH7@ zZ35DpU;^~&->wa=>c>;V`o04@ii*L6pdbCf`fBU&yV=-uw_lE}q*Y|GGaFWG>=t<$ zq*XG->(in|Q^8!ZlFqf*(;`$7a+Oj2j%EbJYM*jO2>*I>A#Z#y-|HM8&BLYH%C@q5 z=XJHx0=h)rpPHSg`VI3wXE?bD5fLs0d8HD9q=4%|`pgh|_nOM;bj$|Kd8oU<>~6+g zhKzSoayKlPp>{7SnhR$b}FE=tVLK`vuopl;Gy`;pM zRL;D{)J4wpEPnbAc8v+<4bi`8)ulhVsSuX6g5=)6KFLKq@!F%+szJ4LKK934b)jn)6$~5pGFcQjXq-Eqb{m)HVgk+Q*nFX z{+kOb^bMZg!iix0e`lIL+8Bxz0(!G#b4v>(kk~$vp*dolzAEd8EzK2&SQRPY;t%bD zX@!0Z%7WdtS@@x#omzy!)+YY-8;WlhnRuvyNCdW%; zRnLgqHqtfstLMg%sHkY;*5xiq18h>8C{++|K0-l~B)8 zpl+d&|DLR+Jh=yNsc!MzB;>zBNe zF^#!v?=ZSlfJBHz5Wxc7dX16loT=iTJlL+y*cT)z{Wf!{rJw$yMfQ%?j#sI zNcKVX&lEx3#mZAE?St+H<&13(3mT!8BMN%rIJBODpk|gKT4ZtdfS5kk=h0fAIyJf^ zaoP935lt(UquZGgPpDU7X+fdaob$Lvn_B5_q$GsZI-S~xA6Sfa@zQUYm5`s2;e)g| zS=%PQaki=o&NJ}3xudGPhgLVw%&}^gf{B>mR^e2{< zUgo8)?>2ooV6iNYhG(f?x~S4JfL;}}R&LE{Ma4n6(*txr*5)ToltrpMS<@qNCtege zdCII`yX!Y)$3WAXW*oT9sDT=rgjDCfszF)iV=)yGv+_RV19`9bjE(TquDsj`mEvWn zUH50*fy%JpX$O&M@(LuM(6Q}M=OC0pK)jm@s;mG>JXvF&EXBJO__4{Ft6+m zLYCwqs;MuHU(x$q_l-wwY#&OLR!v64PJo~ax^#a(s*1*(7x}&C!#1T(-5#x2)9FpW zK;-7bsrLtOK!I%^q0^?o+iZCw>XQ+k%5YcS!*tQNLM1hZA}n4O^wV(Qi6|OiqDmqc z1zUTv`)8*tyx%gEcFc~x_k2p|JZ6@pmQO5|%|@;De2Jxk4B$EZIWkAC`|yzAiI21{ zq|b3gfXJ%R>4^LjAs?$M$|VMYt{**~3Pvy8lU(4BmCf9hG_5gMs>=wyHSV$2Amh_o z56Ls>4!BPa%T(TZe=ff?#DBU9O!`}7yla&BgQ|6C z`8K9e1<0s27rNuB&ERF_L_ovymgYep5k^sM@FMiNfIO0gl64RF>AKGrA{Re&M-_o~ z(qN_1=Nv@56WH|Z!sKCDt!vaMfTrO3`Pt!f^!$#ZljTXMWw^y_4Mlgy>Ae7FWg@Z1 za32gst(pr>0C!Hk4%rgTI(tIcM%4=2g*6v9Se1uYWlCbefYJ1N*g;Ou^Tm4* z^D04@j#A&JpdSJJ6Xy-*v&8LHskuXR5?a8b&p${GAu}zve7>qDB#^KS#UA(&jiOXg zYZG*ABY>OjkS^->H^J)>vb@)Lb1D zFrCu^d;!oY2APk6B+x-tLLEv#Rka`{x z(W4c_<%%kx_dE?=zxIqmf}g4cK4T*}St0A!BCC^?P(l|uH|Xjgf%`ScJUfen!S5e) zu`g~7pW|Zb{nYQKb#!`)hZQf$Wg^yCHj-V}>e}`|A+|7Gyo=@!02sdlGKHzsd|h94 zPDG~QM5dJWr`3*xoEuO@Vv)_QTRi7!k9M?}cXn(cE9 zn4;BEI48HbI%l^iO%r}eV%hsWe_^s3y)B zcM8^nBiWnh+#db7iWYu*_Lw2r0?^7p8=PnDM2&uM(sxk~~-8t1^-e zZe8Gora#L+c74OK29^ckVLUEC1>U3l>gg{x=KserF1C{qb~zGX4{ngfBA#Tl8EK3> z-Ip560R1r=rhw~M)2h8*U~62MhtbjM#ke{oc+lWuHqXnm0e;HtM1?6Ijk#?7QGHqO z_J~Xlc*Q62Z-u7pIvSC1--rwy7?$dlX1I+n>|HHRADI@-$5*%;_3n~#V~&49fO^cqH=mmml_2f?$jcXuKK|j;EKQm1FObdL zIv%hc+3h%3S=n>6T?r}$WI@|)ZI9}83Ekd&*dnpFAPcdVWE@d}5WHzm1JSN2;r(8) z32%Qyx8;TzUzS56fUyI-tUX81#v*Q~PyB-S6oLi9Rzf|_w zR64<$X#o7um~o=sZ!+Q5)pb7e5jC~*BG=6ugA)mG)X1hh=~Ie|k<*z4r=oMpn8BkB z2_v6#uJbv5r|U)S@+5l_&pSDpUYU<-c3A%twNP>76&c%tgVaDCr4SRMrkG7v`N0hF zE?;~8e)GPiy4q19E*4Iv4bqIp2vXx@3fCie-SGOWorHg5d&<+dsytAR{3kT>RScwY zDE2TvL6Nhza1O_>dlvr;WsG@prHwos@bB=^v_CL~pHrwCk6hJd;K|m=g$y=0eEUPy zuOmbuaNwTsoZBqY5`Qbq-L!B8FI{#ZtNe?caQBXM5J5N5TZwGc=l%9QVxUWIn9=3H z|D-L{d@2(mj3}>D5G=|>-KVzX8k;1uY4Y`{^Dv%)A4RGeed3JZ1p)apzlh(w~z-}k`FxQO#a~nd;9v{ya>T1Qi`aW;=k)_0= zv165mH}3C0tjB#Ot4dv4d&_CrTgt}52-1!N{*5b(RBgI7kQ8}46ZCl?8Ko5s_3dvO zPHIl=RXSAOkS5rz@{s5$Ww|l$e~QTU_{cwyXOdmltxLO16hfEMD(SO^^3~8FKP>DP zt>u%zNxL38Vo<9lvkSHB3FrK$Oa|?bxZebNP=21rK2re~)m_7|mIZQ&$?ol+e(eyz zK2e^u6w~|vlG%N@(b~?&2}_e+HE(dC*iM-m=a?F<=X0f=w3p&vo}aRU-B=hI#iStY zK>|+!UrIeV_4P~{lDZM4z!~5tVvS{DMRwCa>VN5L!Nw*QkByi!BeYEGZw?%V(7Td6g3Bz+>Ls<)c2%P3Mc;@HEYs zSs}kvWd+9i4*_QdDrV~Z+Xkegh<+O+5E&>MnE@mdnYl%8eSVyyBXWca70t-3&Nt|x z1(7~w5v#NDPRef|b?H9j=Q|x6UHM!g`tJV=Nx<)3%PQ%4kKOxvj_EKOeCEKn3`se3 z-_GX;^wV=q;tN-5hgJhWaUgN1M_1RtOiyq0x?|WVvU4Y2<4{mLhmHkuAn*IrK)jM( z)apxJPd9Pg@4%{kHvi4S?df96olFE7!gfHhn1eJvHLfah_YF3WM)EQLd|Nr<#)3ps zaN;K*q@K!ZHjGS;ormJ)fak)Tv`O+`!aGz-TPC-|Fde#!+Vi5xheqHE6g`Y5jaO>+kZMB>B-?L9n z_1a`E|CAn&@5D*`(no|u@yhu{kmZK$$ZoYj?irqRk)4w(}=l zR|H)dmKc=)G38(U*uUebsVdDRjAcEp`*eegP3`oFas2d}d0hrBaM9SfAJT6C92b&- z055eyv@8%33e!5g+!eFBwPAQYU9d_ju^I`Q5FY^G!pQWJ`A7b z67qEVy)ihhMiy<$;B(9cSMrel6$MnAcXH%zZ|GbSg`Afqk5&?7 z61{e$<)}Mr-{9F{9!Ci&;eGTvcLz{P{|?}kwbxbeKo zKpu|GWo50v!GOnkODz0G_7sQjQ!}oOeNH9~!SoGAg^SnfkBnd8k`+sd zCAms&m$zAwZ*CE$$P!Z(C2fG}Cr(-)q}|?PO?aJ!81Fza!hIN5CFm5_e#T7xphMH_ z;MPaoLG`(J_~0wv)zhM?)#39mu8`6$)F5iO+%87$p7X@sPRu@vPZqzplSr?&_&T$Y z6csnR%c7H`ImKSVx%f!=RtXeMr?Jdon>lP@Q(ZL|bzZX@VD(C=a9tRZ(8?{w@-#T6 zkbh{5F35{^dHE*y<|pO@Q3g)lxs8Z@J6D6Ow$Dx7vSWP9D4%@FR zy>^d+V#&Sl!_?%9W#PH;^)!<(1J(3GUuu&x_r_TMV1^ zNIhfYNlU=}F}+7N{Xk-5tnE)I(!jz`>jVPozu(P z*}}(T??@Gjm<Qc$~ir6_c^SMCdm#?eR$;*H059P14sVncLgBP2yHqDzWF7CO=Y;11r8G2x?lnAfY?j|hAx&V;XL}}x5?qZ2- znM{RtN@cRcY6p9di>@NM}%d|OgKYuIq2 z9|0R57?2OG7UgY}OMltpt8TCS+XWaHT_)OXr{ycnt%)dcUnd0Adds*dlVo-|^i~WD zrd}3<_49?D@Lxi;_YypOpTLhTfvH?x#pbp!`b_T|5t0(+Lj!pAmA@>d)7(yKR;xgm zyw_lOf@hSdh%ITRFQs3wbnrAS z+97=l?^z@z_gNHVVJ6?7d2;{N0bddxZXsAt&9D>at-`K~Z0XtekzVaP<+;S9Qsrgd z?xY&|mV_q6w&!lJi81{OIms32KQgA2diUp63Zc$wQ8|XO!gS9X5m`kN$!BpiQ4lg` zg$3z2_#US)Vs{(s65$E-%DV!8Icgt^QC+~utlJ-d>fj)6q9wd2{gRrq$~?eCjDZ)! z^(>ZUuKfym>AW+L>p)!OADILE%_BtCc4xvmglE>sJ{cttz#o3tPOHZcW9a)(v4ne8 zEG{9%IAeL?cU@0?1G}e2sx?UtwQ-!Qj%)OR)w^(`k+>EDu*fmm)G{^-_+5T^eSVV+ z@)mTf{IIeK#z1^pq<@V-S4+P7uV=A=A2Pg;3jlC(q}MV1Es>L_5afN)m*c_}1~S4; z?z=q@@|xGPrXeJYQ}Li#qS;m>S0nx3f_rMm`zzI!eX<`MxPSfhDpA^u*bvz05~uuJ zx!mFxv=!Q5v{Vqo@5A%>y9USizt)Q%)41o{`Ay(5*z3H3g!DEfk3<7(L1%AE_-s7y z6>QXxm9oa$mZ8>b!O~V+DPDDMAPMAEws6!gv9ZIinIz}o?59$c?W&E&{tL<~9~Gum z8Di9T;m1r|E$IqX*apf&km_9D;4c)}KiK|B5_D5{*94O?m~p~2I2nP+DQOiX^U&i( zRaIDIXEuwHhAl6qYQR3uv*mPJ`*sJ;A^m=N^>md{0@-wh37_|D;vsr@A-kx;PE}_@ zR%owWrrv(gEUMw0ngOHp3@an5NWZPNu59+dxq-xgUDo7m;$2rj_%TC``m)uP#agY$ zwZ#Nxa7i6L#kyatZSsEO#%z=;zNKtL9E0UR2V|i(AbYE=%ZsF_oG&FG{9Trm?Uz`p zs=i?JmT3{bH`#ML3m&*MUdQhJzxkYcICe^BIaSKVQi^EUAU*$;Hu7FMf^;npEU`E> zOyHhs=`I8&tx;xMNK$2M;TDwH^$70(-Plke)z;LWzMmPM2svWZ6fxn-Si?&OLf)1?)}VpgMRm=#ECj)BNcD%bs< zj;&OZ7xA~5cp>(7+F}xZ%m|2%+i={jm~3@@@anfz-0qYPtl05`5M}&A=ft1MRGF8P z8{p!4ev&-ub4fG*C_WQ$Sq&V-qgl;=1ds6WrWem(xv|S`OEy zQ=j-L)0WQ|D;{5MqT8@M=6d_X^|o8e81#Vns}`KEhCyvhXpavu=PnI7rF`eXaw;*< zx8p)zis}Zy<&4$ELkB$gvJ7{3I>tp4#{tjU{*$Gt>3*ML>uukH;Ly8cC55KIYJqon zHqDWSchS6pm9z52I6<-YT0))|oQK0u4VeSjF|cAd8MvaIv##5tCI#VQ0;!G0OKb2A zuX$U3Lq^;B)(P~~RvP9759BGWVrI(G_PR2=nw(-<%A4>X#{U}0f}UjnZt;GBUC{M= zvde*ln!+MpG_;4Y;HQ=y>gqZsy%ya`51+P`j6;ooFU zy%y(-+4rVn2NkIYLQD4WZpghnsUO?lQWzc^9lT#8gP~Q z9eAMkLV|^E-osGq_A4u^@u~)we%QrBlH8&U9_6_i^h{QtW_YDUt}n;2^I~P^MixF? zJM-UqO3|2U)xd68WwUh!BL+F8NEnZp-&H__jAfG51GDgXlePIh4ksZd3598v4%PRQ z!rkJnh-094?SY~_CZ^0%Z2r+4Qi@O7WZch#Kr>{4{sM^dh%dt|%e#Q>S1?_Cb%2 z8&`~Skz2xl^>9D+G{^3uC#vP=@_{l7K@+9+Wr+bo8MP) z?VwagQ#~*n>-Vm$>lV?Hyaa0MbbawZO~TIixFl>{_AQ&d!&H0SfXHD=5d>9c|8(Bj zs4-wn5RRF4i8C~5v9I_35g``V2EQl91Kn$8pzHb|xwV})){kqD`iD7P8Wa}I{HV5zFB5uaM)<2H z!!S!4xrJOR86$|+y#UHY*3@p#)0F(3Ge4;$dE}UvekDrQ6!r$$)Wo-hVD)?Cn3C-7 zcoBKVV)7KL0xfN(eN)Q3E6*i$9nURF1JhfY(^!_{YM7U{_{k{ZMIZ7%n&fy{Gcd^o zr{z}-2z^^b#yt60lcr3hFt){o$-4wNTgk668KEH|wb9wB5#GWHRT5U}o0U;OeP^P* zoD$tC##2&WLUY#Y1FX2QT0NfL&+8d+hSQZ@X)TQ#lSmX?jqojb9|~sP zhOJQiPk~prmA+@T-7z7uNm?jxv7dJP< z-HVWU$+?i|)j)A6V7@HtAUVK6SXsTGLG%S$s|+zDR1GK#KnyG@#E+%dR01XwvNwL{ zS8y%UQeLE-YZcg;h?T7O2S75*uCaZ2|0&hU`T8{cL2%yNRSP`Pt#BgFgsWl!mN+aQ z7k}+R{*s$YszdVPRkFes3) z5}hpMo*YnFX1`y?vZkqFbE1}rY}t?~-SiM6K7Q@@u!7@D@*iE-ag#@lR`^t!Caaff z0v#B^_Err`deba)E3nFLtthFPTE288t=nqw=JZ@u4@1N>Pzrff!gN1}Z{LGxcT6_! z*KpE;lHCF0g!H`q9cx(mz*qn_T&l7;vg`usWNTJ!Gg=IpU@9(+q&p`zYw>6Vl9Sv`CHnl z{bTAfLNyRlZfQAN@y~k%<>p_qjJ7N_IhNVJmIu9$q#Jo66oJtl0z}L`kNnmt|0l7(Vm-c`&o1w0Ybllh?dy?Uy(PjlL|D*Ka zt4>Ip8coZ5Qj!q(A%-sP=ZuB4>08_7uuE1W_!i}ODD0}{bH{(FuBcwVyet3UbuD`606jfJ`{X%g;S-BNujgN1{4Z?z|1wB_{?PQf{%I|!iP?XeZj zd`>fQz#QKxzP z^T572rq|D$=~$V3xhJ%0g@O@;Vi^D@V29>3&4?jviY*t$ME7lc)0Sput@or=7;f`a zUsOciLyBn5t7--Mm&?AE*s+T!!cIn23JLy^H_csWexRRkb@u&zn)~{xGmT=E!0@TW zk}9(_OqK^^B~HL5A>YT+xj1waQ6c8v=|5D81ZV>cpYP;SloNddIba z(^d9w3ro^N->Zg?5lRDQ>e1cEUDvPw;D zrb}#Ni-zzu$pCVCQ7#`tZ6~MVy4K5=qur$2k~Fz=+Bp^RE1Pd;+qkelLEH~hbZoXd zOo}0%`Tv`?cMO1vRMx)pAIk3b4R?Lu<53^2CIi4cFVk(FiX zkg9;FulKjpySrR_eXpk&| zh%!1&s%IegC8jH&oXqky?1%I9a&9AEdg5(LtZr?win@omosEC(xLK2YFS88`eG2G7 z<$}e_4@X;RJ2XZYZoUI9RFow`tWNd^%_?WzJlX&ntju_FCe^D#kpJ21PA-qwY+NJw z;_<|N&V%R9zw-^y$25J|nMlXuAwnwzLEb5gJ2*=Ev#5&vkfBcZTOavie}Xf1wo7HL zGUN#Q{46EbSj_Spqt(F6z{OkfE*aIZMuKZ#=lACLUAP7)qpw0Nxn{uvn6l8~T+(PEE zI&6E4XEpk@9qMal>=*$7fE-R{)7xV0yM2gKquC5KghW$>SnwNZ+8irW#gSn``h>5SxuS2! z{9@QiKfT^?0=rCqwOw#|**{0`0&RX(wV^tAHc)AtZSb3D>=yA3*et)>*}Cx$hHRy5 zy|_)NiZsb8O}}GqJtSY@S6(nHPp*vl5*yv#L;fIqd@k5sF?#$9*1dVWvp*UbyL`Rz zE~vNe|@DSH%iTncmzP91h+?$vx(Tcr$}1KB>DXU4zu zGG6F@fn4x6?hH)i=3m3FVD#IvpF61jszi{K&3)Cm+vb65XcgiEC-}Zwn9cKEDs~5@ zE(HT5cvGX#u)^4sv@+2%T*Wtj@yWOZ>o%nU|E`DTG$f@BL45B?OojB(`ouKVs!5Sg z9QHo06VbGBRjjj9?wiHf;i-;!Y!6m7Y@63wW&+>qdw|ku`(uH+pa&swH{aVA9k){m z1st&}_Rk%!{$rG*^$rK>***n!m#@w^(z29kHilIx=?(ydMZG8TI3chwMD>yd}9Je^w zXmlWK5;C`|A$A{3N!O@)E+0CI^Oy8(Mq{aA3C`O?uSzU<@;2jF&c00x-l(QXdu;sL zRBbz}F%rI~_qlnDMQ~*WLJ=MGDw8>a_Cr{Y?MND=kHu}D{ ztp#fBCN!2+^}8+h=i)BTRvtp0Pgo-aPENrQf{;Q5*2=DX=h@sZM##vq7*w0BMpHJH z0)pRGVVpT8M}Qo4&M6B|VQ)i5H%Eddn^DK2cYq=Wu#wE}mJv57K8RyKyrlmUi~upO z$APrNdCWjVp+}7r_6dB(1_DtH#RE_JqEy?Jt^M-=S_f-IZ>KY}J29f^W7K?F*|247 zAz9b?K^zKuOumCh@ehE2>Ss&c?y^k^S_Dvem zI@zj-&jWUfIrDyI%Z$g!;HQPO(B_KFX?KIt(X*Xhs_hv|bkFViuE|XC2p+et7Hu9YO}kcsLb%esV5GfJ84r= z_xr-h4`&3Y>>?~GJ9oy|TXmVguWxbP01zbpm$+i@Hn zA5gwqoG0PAH-{(?obPx4U}-SaQ`|G~XdSe5F!+KL(b!p-uxQ;*ax@r>2*S)>aSl6ezng z604wU&LBF7gopa2(o@s6(*~z+b~1;{CJ!-Y>{$Pahy)ce>X+KM+^XF3OX~%=0gCg~87a)YNheDOO=Ph?)MUE)ILNOmb~bulDKF$Ue5Ii8gM+{DMj$H_#F z&#W^FJdJDFbF;tOSb2H1_m?Bk6_(9$bA^?71sT7ihRt1CDE!Td6SJ3-zuF$YO~A)o zRxrYrRpS|JOCt~Z#RmS8oz1*rCe*8oEhneR<%iR8+WHZ~EF2Z|i~}bTBCA zjqv!N75*@${L56AQ4J~rNNx1yjg48mVYJG($fD&6*ay6FB)3+@#{A&NgZr_f!}P{j zp~L1R%@&n*sr}uai)58iw+qPn`FL^fLln{H4z6ZJDkM^ZR4HO{2I_ zVpX@}^uzX!0drZIGT2LUXA*cno6B7A#sEApRh_x(_RlMqkz|(#6H?Ui6sz2laPdB# z$L0R;4xzui1+`6Iha#$)v?0tXqy`p|x1_?!K_)J%$8(oq>f$EO*YZ+|?;Y&O`()|v zR0yYSMAR1sE(h0lbyP;Iuf4G0ZJ5N@VDTVS&;#Y!uY9}dEL#5S4fA>5-4wSWPkZmg z#gVIOva0et=0sjv`V&v)z`V1qp>`H6)c4#a4 za6=ssCJ&-Z#+@DfdvRwLjS;jex3;a9mS*p+or>3C4cVo`Iva;_;rEX_->+0UC03SH zcm{oJuaXVv#)~J;EirGOc0?rvo=C|72g}6CNGI7B)??fF5k`hBrPuuHoI@&lhAULB zejsTYY8+jXIrF$DBPZ3MTOe6zuC5Z*ZJy)~w&~UcoO%b`%=~)y&m(|Nqy%qDSX2X3 z8=aO%h-L}^KJ?ElhH@2{4B%jM#Myq)WKCB2DApzObesQ7->-yU9^YUoJIff}(n4H9 z-z=BbRrQ(_SDfyE=Cmg$e;HeS;~yxd(cVm@D%SFW$2XF#+J543zsXwp{o5+Ea=!r8 zxhp3TVuY+O0-Gj4e&akY2HgU5`<5B&r&Gt(!THru9PZM&G2MSr2Hg{$dn@|V{=SKP zB}rgCuaeV5N7khA&euV&C^vhds^ijJ?oq|TtbPxbvP5!fPN6?`xW~d~vjHru(4U)l zE_ie70cl(E6U06t0u&8FWUq&@edxAJ>SHF)=7`=Vzu@TM(r83t^oKLQbli-VdN??M zXCxRd{0k&b{Hn|L4mKyeu-sbB0;}n`65L)^JbhCnr8oIAp@3LLvRh6>fz#%B7msDU zoBlHn;vi&ne~pW`cjfGN7;*t2oh)C_``!k9VU^~&QliKnqT)2_X&i_2t7wejBv`yk zd<&_UgQ#9-cURv01+(C4%PZq&v#&Y-7h~ z8Tu!D0>7o_L2AcVf~eWZ?}O@2u@%xR0*YBd@#^DrK`1V_9ysy3&vEB(k=CS_1Fr5r z9w;`g{wXXB|M+nIJvfN>`;mr{-oE+D<~mau zIz*Xh@Ez*2w_2z{OUb#c$~crM-vT*)oO5I5o!);Bky)O;cY2U|jb!>DK9-o@((-yk zm3L({pn}Jpy*hY{a-bG5A+R{=}*}ey(W9ixYb$&%XcgWbaDH<t?KcoYmi?G1~Bz+ zYkaN05!nuUneA{#215r}g`v*~MMTa&<{Dj;@b%wr%JP}{oHvK&51I)dBnPET6jvl2 zzm%^1G>t=r8OZw-^(AJhBSWbXQUMCG-abedA03;Its}G!L6G~fjhIga{f|xARA>eJ znnB>>%b-RXKJX8=;K;z#c-oCse4gKY=dMPz`)$a3jR&yTq}$C`YguFYDzgO=<{a`L zZ-m%uRQFV1LnE_L4jcfsP~l_U1h zz|9L378T%A(qVr+@&A~53x_JV<_&c3O^2j(BOxK7bf5nd?-I9k2&mYjI{`e;+i&$b zKp0^UR8US#3f~idqmuuNB9gyGzDow*E_aOx;JI5yuKPKll84k;&wdg9G8%OQ+B3}q ziFOAA44xXIH?L{L3E{jxj9u{thUvhR{qZ5Odz%eA;ttA}yFSklcXvuLapyE*@7c^k zYj2#J7YB2YtCm>li)yMSk0Vi|$mN!BR69Hy(n;H3QhNbyPeBV2BMK&gPj^_b6Eb*~ zir+3_wC_~}f=|T1HywJ}HTj~74u*Af{+;*?gtR{l46@Oihx7B<3rR@!*2@sUr^Ccv zy{?L(`D&VrY$StfK?ukD1QC5%rhEx2;Nov$i{dC3m3BS26Yu!64eP=K^gs8OQu(_h zhAX4^W6ZcZDJ=teXm9#XlMMW4hv2oOj{12iPpQd?#I*(VJA5C zU;X7L;{&L%KDXcddRqMqjW`q)Inc8C4>Qln2;iyhx3BN!?a!7?6VD))Q&a(TwcEK{ zRG%P0EeUyCiU)O8|FSNkWD}3OBY4)c9gv|nS zCG)hvOev#A*$4#OS^l?G%W7bMH?%6Mx`%U5qR<+x^G82&_GaA4;;>l7$-er|y5gUj zki@!B@ODG*i%}hh{O|yK=FZM)DQiB6<3d*qm5|@AyXJ@sgU7@~fd=g9AV&dQK*%jK zmH#*@H6vnvpwZR3`meZy2QhL*Y_T=n)z0LXAY%Ogj)>g|{p;fS_-*2_N-M@Cf`7!v z6)iYlhRAtgc_%R))#}Al6J}N8VqoP3JKG8Ap(P~!Wd!gH9Y_u)RY6p&`K72(d&e<& zl6|@{=hsW@0Id=MHH{_ovKaP=lrntn$_9<-RR^er?=Jc{Zt%o9*2OfFSOst<9|Lsc z=&S`(THO(I#aasE|AMT70_IuPye$z@JMM^_w?;mgw*MazwV`T!FBgSWL(Z<~-Mjg# zls3{dAvPTyl4y@20l@_xA{bzb`~4%xpr2fpUvJywrL+~|@h0n&hX66<==_G@;m)Ys zlNLvh3mn&6HNu9tLpTM%AwqrX+1ZLS)-^o}nqU>ivPlGtERSG)q<(7CtHEEq^_CuE zdjEiDs$scVUA7*@t{N^YI|A1`JpU2GV)Y%^Scy7m^YSM@_Qpc0eOIq#c1;T4zg(0} zb6UAITnB&kWJ7k~pSEQ8uD(_IK)MecjQBEVvF4TUoed!gm(LJLI4`| zE$}}8`ymdBge?2ls3D)yw8V}7u0lcR^MB!tp!2JqTZhkPVy~sfkm$tsxk)yh&Yu{J z9!9{}jO{{Z1)zWPdo)5oX=pu_=&3yfTHE+HBxka8q=Nw{)(1A-+Ysi;rr&XK6W1=L z<&LY#kxj?yZDwKfytu7m*Vt4KEEQN=0A>da%uJWw{#gP086sHNKzPqEUtHjfcqx%f z7L>XDIJ1$GlHzIotmFatB9Vo_Yic4yViU{CD zcB$lPqu45Zo+qgFg5Mog7IJr=9aPW(MqmKLb9A`<|HXZ$c`N(sjbtjJSAyjk6@suT zw}<(D@Gy-HV(_5i6o!=B)5N?ImgRBq^aPE=pp(NYllM0^$BF>5taxEgA#xswY~b40 zz&R%CzK)oczvy~BjX~O=&c=e)RK&?Ze-eRj?iB3bC7YYu(}}99UgMZE`ja^epMj3l zhm?*j@}&WzRbqdAto>+dgc()y`KHi-;UQMm1d@KFgS_jWyP&g*s;gO|Y=23O9KnNm z`+ot}r+3lwROjJw+RIUQn7Q&{z8%WLv+Hw8w}bk*4;{EU>>T@7yF7|hI`Qz(I^!KvLMU( z|LK)4&&`xk!ak{53(oP~wtjfPpMQ~p`U3F1&o%1!*W#DBZ_W(EY(#xXdDsnP=AX1| zO2N}#z!#a$`lNM}vAsC5xtCb3#T`TUB|xVMP3iEZ=NxsxBWx?aXdX8Eyx!8wp5OAX z#%`#JigzctARpXB_t~>ebahI1>%ZiAS zzns6}H3gNV8=0B|&zs)*YeX94gmj=B;L#0k8CHUR)BEFQeg?kw+9hy(>`mS`^#49@ z6cz@%P|5q?e)a!5zsd7Hd)M+htjfJi;`nACnGH^jHYKeraq`wXOi3iGI!|R~$spu2 zq>c-#?g0DOS+XQhfyzC@@8CPcFb!3TrENs04y%=`R2V+W(B;k|vkhmq#yv>NOvN+NUD0NXq z4LbaN2Vgt)qrfZC0CnZT=v>2V8vb-S_&}NXM6RXFk^kdiA~*tT=l$!xsq^fu-<9>$ zHJ^_^zQ!-vG}6187%Dh>a>=C=@9TJjh2)q;oL<}M=VyhV&ak(^b5m5B=Y$qHr5)2Y z!cx;W83{jxX2bb^Wr4SQ{z-aaF*99Y(6l>8R$K3f53wV|v3^IJpA#b9w6%IOthFVF z_*ECS{zQ+HmG8AE9!?jnmjqmI$r~1zmht)*MwV`{KF-P2Ou9U#1adPSXZDoCx1|?6 zx4EzX@7!y=M;9G!S9toc40;O1kX(ea-)Em})apF%6Vl#dfQiC2W(~lAby>_5j2ZcaT~;vcXFI-(IG{=F_el?2IPw3OTT@yas?Svd0z^1mX#VXK9q+Ysac{X4MEv&qv^MVaSS~hx zuy+ndu#?}k82^hlD?tZU@XDNbo5@Aj6;-p!9?x@WOSsfH0*bl&KZ0EFT*%8(`=es9 z()~6KUg-zFmP|a?l8jJFlonMbk;x^CGNp5G!ErcU`Ld9mzc{m9!2sVMAFH)df+<1B zJW&2gow${V04tBn)yx$BOUN{++X9CVWbJ%JBb%{Y8H=81!w7W6krqnqO(QZIKq}jm zr=^)423)Wi6)XWAr&25ev>tONelrBO-&!p;wFAGcdl81lv?DUML-tiE1-vKEga3p2 z*c#tmA$L)LQ>XHQF;1m87^5 z(n4c5QgD7hTk)!0>K_9F6Wb_%fk!VYM!tHssHnOXdxIYjyl3zKNmL?1Xh3__GcFm9 zx?D>y+pryXQsDfX#SOa2bA|0hWp#>Apx(jp`?msd-JIX{e+NejTYpKB2Tg?Aefcv> z?$5T4dB7dkNmpVAY=zW!_?sC)$Q2d!^#1jq{`2>`XJ9C#&hdHLO_jguBN#!0Ndl$N z;%8@}GCYEx-q`BO_XUh5suhIxwTw zR+T*MehO@h#O;^+tVw^XrvlnmJs!1aFwPWyb^8ta z!S1$^9|#NVyyw4;`XBMKm)In%Xn(+5^YN8_BXz^iCS8p1v-W}UHd$UQ$N)}W$H0I^ zjnjH}UA`<@cXl<8WGqXb&Kw1gtFhN7gza<>PHw6;W_^m*y15vaYW1|%d}+b{e5`}t z!H@32>}bZs9@sA~g)&HuR#O8|9?7L<4bJc%SWkOia>T9g7m^_|vGnp|lMG#Os@)sMW-=!5%Jcl6iWonRQp z!27$5Uy_=?SM^ItvHTg3o|U!NH!|V}9W*w{JWz6MPWFs?&g2%oCEJnLLr`U@jx4;^ zmU9hIo)SFS3u&RnQ$qhV``JMpjj}0geR$&?$+EY}g@#MXSP;v$^Q#e@+Vp?3-^BoHJGFP-5fEU#9*dVKWa)^jRWv7t3iC!A+|+?08)h4zP-BItaSd1HIJi!|efRkfoW zS=yZd;{C+8Zl?#kmdNI-y+;E6-04fT3#4`mc-brn$T|N+U8V=vam%`rKM}ffSm})d z-(pe;)FNU-H*+K`PjtpaDD%`z%nccw2*Cm=TvVW z2utiVX67aa*mY>AiT~aG`uc4{HhA0krP^{*;TQ})`YF`e-A&HQ2I@M=r?TOB+?rcO zH7oEdKO9MGymDV8%2g*#LbOgTGmNEUU*t4l=5R%4z4}Ty(%R|K>Y~WHB}|x<{~7f9 zG+Hb^zY^rA18H~%j6Mylz(+8IKh19h9O834F5pj+2s%xkfB%nWi4dd0uXP@B_G=6j zT3XH#m(f*M6{pp~lQ{9N30yW2qLQQgv;hK8$>rhp|cexaKHg%LNIH<~aXCWp-$VCML$l!u7O6#fLghpUCc!|G_>Qa!l_6sb!$* z-rmDCd`~X;jn|ana-h$kG19 zbATt$qfH=qFR*e?t$d}6hVi(u%h*8*B*ghrjKs#ezwA!Z-|vZ0J+djO;bY@cNlq%# z{cWX&rFJ-0Vk;LvLJthkYPA3Sm_+1Sc`W~!JB!C2+fG-B=P0WSoJ0pxsFdvaQyYFa znPma+8nqyjRdDd%>n;(}0YN1$jKfaajyDz-K*9kxDMZOJl?>M~N+7F-i{acqXDkUXQdw? zJ_3F7*qnt*&3WKP<>UWTh2KX}qYaDVNJ*93=aaGT4-;oyKN?t8od|e)t?z}l3dhV0 zGg&7B9%)5kU!5^Fz8C{j`;{+-ZVi@xXtS7@5xJB9mDD*sSBn=QemnAr$fx2QH7A&XEsU=#j&m}hV0>?aZc31Vtuqs~rXfMSTcjd=Kk5ystP9$a#9i{^z*S*a)kq>ju6A4js1w z>pph$VPVrT*JfGAD=84muJx3s>)dqwCIU$F5POgaSoIlv_dVwR4fn$`5!|j?^odx~ zs*LrC!q_p{{Bpf;GrWsRAk%(*Nm=zCtxSzX7|WG3X8LfZ&eVPQ!N=Mf3$(h;q`o5gd0X&wFYeW-@0SpDP9 z|2$2z_?-6TbWR3^Z7vW|g`FWVRxZMl56o8J7q{%MhF6x0 zSq8yBZe_?v#p+ylA3Gi9ko|3UAI#s`K(?HDY|7#xz*@_%2%;QV`5)wygC8nA z2R%6LSt5t^vM0`sIl;DNot9j5%8M4m z1zL^}xboUmfX~(znc&fbgwG$y_r8$xYk{|2tn$ z4GtrcfLq*LB%rHnLG06v*ba|)uFj!Pri0D%)ZJ&^U+qp9Rc#lEJR}#LmLf=Ru!WI# z^H8uKDnIRf1`^9N!+wxf-d1%#Kn>p2yYf58?>RxR32 zEIvFMcMF}|e4K&D{JwRC-Pp$P65WBVmv4E`j9$XQhb}FL>eDtmE1-MCNQpg`^dPHXw))SlteMoC#c(@*km+&;JSNenj}Hik6#WNb9|vD{Xw6Xl%(Y#;j$`XL%S{Dp_d zVg`K$Shrje5$-jPO^@&N2`HlJf5j1LW6=}T6EbwQE_6nU^O!|AeWO5u`!)Ub8{<+J zLxlr^siK#8$juRTSzn-(!l)0duoO8opnbHv-Pkk>uY?k4I5JUtGyu`4NAI*D0K{P) zA*x=JyWc@r71s3gTGI@ zt8#gR149Z3awAZt-HmdmqF&`3v?yib$cQ@ui-pye{rD5F(fosT(;&9h%MNbndwoMZb2zsOTyVA)x z5Ce&5O-C3v=3-dkfYEAGG@w7nG58igeAns_HIOWf!erQev_>YMfXjSl1|Xgn;HNp? zw!h=lK3@4X+E(GO4PrJO*{Xr>@!EAoqS0RJ4>K+GY_~y9^BPixbm^awkCVkoX2$*g zQ3h-g#{=&*sQC)b`kxP?k}zj40Ll{EebHmP2xyRNn-wWJ2CGHT!Smw`k)yloNi3`f z9BOX^?OlxF+28WW8_!QCB$1sEjqYc<~EgXVFwVz?ac_OgD&(1ht=HC~t(0%8K z+x^#_=+`98rhMQhZ-EjJ&XEevn;jGXgXU<&r;!CD)c7{oMx4ScDS`V zKi8FHn%T*%glt))@ApV9l?l;B(Ea(|>O{L1C6!O~Gs?5EClxl9ZbAox!%r>*u|!_& zv{#+KzbVZO&w+L%p|FB0bFTwqr-`0LnzhxYjDhV`np@6RAGdUmDi5?(q0L=pgYNg^ zU!dCPt_Ueu@9%y^aegm#>2iy>S7mc0?u!$42Z$hppo>j_02icS|ID~Qp17PK&t?n( zWpdPRVq-J^FTXAR0LTy*WDO5`U2?q;47;Z~;Ov=bMAaQ0D1_^+2+Iy_7|?pbcVMx9 z%=O}5JF&FeiyyQpMb$dt^P)j>k9fhfDo;@u5`n$LY3z8;hp6k8$_EqlkhJ58Go14u zOA{7Qz5^qe4Du6%a-Ra|xq}GSNlWb{fsYVJDMSE#T}=?f%)%_l3#gue)6_H>7W$97 zjma%10;BEA%EewkN)5c8`#q*1Crl!uZBdTs7u9Pb!ziLp9))1vLA0X7=b-8L5MHmu zA`(D)>*kg`6q9KCx=Rua&~M+R@m(DV>UdJ))2$d5bnvk7NxmR68#FQ~^tAh?$mr_N zy0^UQ%pJTKjg>eRi+GecLgU^wpsp^{*&a1s*%>P=7M{Q{X|huT5Llqia=l5vZkn_i z?Bzvq(oOq9$ejnmS zVr~$!OoV2DCC3>)P(F3hrHnYQL zB6F-`XJ-{hQt{^Hl|g8_vR~&=EBO2xBy{+dDN!JmjnFJq(da_6xy6OZa{a*RnEWDR zgy^B|{wx{0+(p*x8H1cWDP^a6D1&VbFu7wx7b7}z381|2sNMvM?o6{i96aW=csyr| z@=(gK~a^QC{NHzT;LBZY;W&|1Nz^d^HVE-2DaJ%|o%Xhxf9g;;7O<(BG(NF)x zb$r-KbD^@Yb&g&o5Dh(`pom+L#zLX4$hlhB-Dno;6eWVOEjmb`y6XQ@%XoQ?k%@V= z%vQ_qABBF!t!Q-Z{OPZHVCSvllmsaq3)cMLZcbJ(c3cfT(9q9MD(pz3`E=soc0ivJ z_`FJT;&&Xg{M4U?SA8np@r}qQvjHdYzwozV#=IGrvE(o$@3~5yFn&yGZx;x?JV}7F zQu=bmO~BAU9w`Ib+dBqx6vkHA7a?i4D!OPrK6i(8?>YtVU_^3P#c0srEm8XWf*h`2 zRH$VBe(Wc%BsBQ5_p4&;jHrkw{J*H2y%>1;!mZsRNLF?m5B}h30rmGXtL?_FhiDnJ z?P)Tr-|Me;H>@L@Z@_mR3u8xd?N;S5H+|KuGb4NisguAr+pKmHAM zb`p@*o;$Tpeh17vc2wMj2#V=(8^PiO3~hQF(_CY2pBJYkY8Pa1}I z7|h%gY0rHeNY$UvOpK1d>>2AulD8ck&{Qb9Ao}HXivK`#e9T~{=*a!(CU9KNSseLx z+>4Fkpl@ef5xDVT!Am)#zyhTbmgnMkM0$uY$Gu^E zzS`jEXExzBZ=cjyhf-(b@WW|ioXW)XzjV&ffw4LL?eU6!XBQ1~#B8vb9qRamrwl(M z(HHtw#PmS0WH!|2itz1~m;`o6Q8l8k+>WJ5WQi@{C&QO7%-%p)^vwqs@((l6wrez! zI+u^&!uf^eO>%3tK^`OoNO(XM%J_~bgV$JcTEd8#IWyStja+{D)L8dHph`Jz)7KBa zMXkaEqwgC3L=sm|Xe^+i8sS{zw7un68M~nGWQ3oDJ;E;EXdS}N!~}GumcLH#*u4E8 za&YIGW+Mzp(6nIl`{lixVjWHf}++SA0x5YRG- z0ShBJI>}0?gQx|^a61RsWb-@y z`MoY5Ms&y@-fsJVY!Lx+LW4VkBOPycktpWJ-sxJ13BmD77%fHM*uy<~btpTA>S6Ov zOq3l0dfhKi$py^l>5*bpA3`K_(EB`EF!Wc}g81~rGAH!Y#uQeX*y>#uj63^5UkEO` zM*0y_Qd0lDET7eo03nPZFLHLI$vTdAIqxnJfZtm6O$-gXccaXRC)kBGZrW#_haH%)z|gz=5xP#uFWuJSIh#Q4L0c&vnJ~h0!^M zKRRMG;jCt*Kz!pZo{mg+Oy!n+Xlt2qhHhx0#3z8U{$iOcIs(dpBU^t^F za+St5dKd=B(H+SEj{g2T8L_y8K%Fa~C-B)82UTR~+)x`xc?h{x|IEQBlBjl!A%9Ny zfgKeZ!*anaDpmq|>!1A!I2iiv{Oc>Em_4(MHdUZB(s9A5uSkvUV`g(y2u zn373QNG}%Qt2bOKK|y|&F-|sTO>@_Jyq(I{0*ifd)I2db`x{S9#IGy7Ia~$j`%0DUB){U4Ess|GV&(7(tHoy(1AJxTUO5(rOw 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 emy;JjFXvw)9L=}O@|l|e0000eAnfB_�tgZU zTFe4Nm;f(%812Jv`F>U~H|NsC026X>*vj3#b|2~xe8i4-}c>nSE z|Ig(A%HjWcwEy?||MdC)f4Bc=tN$~O|1gaI=<@&4=KsXr|9{5B^W*>k01$LiPE!DL zgl%j+1+;%}Xho!=Q&N{R^t}K80D?(GK~xyiZH`wKgdhw>$H86$V#TieU!w`4M~}Xr zOP))>&HTTXpQ3+h2NzTe UgN~k=m;e9(07*qoM6N<$f=%R#eUBBYS`}Y4!Rckss>SojZC|3Zicxp>?@`5NVnlBW76t$K@pSz-WpB3*N zao^n-uL7u!M4~?VND|KMf?HE!_jPOg0Su0XF{)#2f&jpXw6_J01Djwt4HF+=3(VX`k}7p_jixgH%8gBPEWz0kBq+mh2T_)F&EYd+1ofkD;Ee}SKR+I!c- z<#BMl5O~iznAN^~ApkP!0i?h$M<(rWAa@v!-H}$$r+;8WCmh%IZzrHq*Mf3O!83yo zp-opD*ainT3+2ReIC-yWjTOhK0eCZS-_-Yd>ONf3Ewo4F?a(#(m>OpCO>%_6boP;1 zcz)Z9ZR`mGz86JXiCLJqtKQ!n3jxZro{`y+wpR70)Z*3MSW#Q1VhE?;P-XvTdZk{E tW_6BR10<-nQT*yKIh4`YOzXO>8WC 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 b*WWN4QtqF=Dsb+fjK7(Yff<&phmZOnXjx(N literal 14429 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 S*^ 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 literal 37270 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

    ; } - event click $(#fix-wayland) { - handler.fix_login_wayland(); - app.update(); - } - event click $(#help-me) { handler.open_url(translate("doc_fix_wayland")); } @@ -774,14 +769,6 @@ class ModifyDefaultLogin: Reactor.Component {
    ; } - event click $(#modify-default-login) { - if (var r = handler.modify_default_login()) { - // without handler, will fail, fucking stupid sciter - handler.msgbox("custom-error", "Error", r); - } - app.update(); - } - event click $(#help-me) { handler.open_url(translate("doc_fix_wayland")); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 403951eaa..4e0fd7744 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -614,12 +614,6 @@ pub fn is_login_wayland() -> bool { return false; } -#[inline] -pub fn fix_login_wayland() { - #[cfg(target_os = "linux")] - crate::platform::linux::fix_login_wayland(); -} - #[inline] pub fn current_is_wayland() -> bool { #[cfg(target_os = "linux")] @@ -628,14 +622,6 @@ pub fn current_is_wayland() -> bool { return false; } -#[inline] -pub fn modify_default_login() -> String { - #[cfg(target_os = "linux")] - return crate::platform::linux::modify_default_login(); - #[cfg(not(target_os = "linux"))] - return "".to_owned(); -} - #[inline] pub fn get_software_update_url() -> String { SOFTWARE_UPDATE_URL.lock().unwrap().clone() From baa30a49b9ace2e45831b6162baa5b511bfd4954 Mon Sep 17 00:00:00 2001 From: Simon Spannagel Date: Mon, 30 Jan 2023 08:39:54 +0100 Subject: [PATCH 263/734] Remove remnant documentation for wayland fix --- src/lang/en.rs | 1 - src/ui/index.tis | 30 ------------------------------ 2 files changed, 31 deletions(-) diff --git a/src/lang/en.rs b/src/lang/en.rs index 6eed43a77..bacef699c 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -25,7 +25,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "The current Android version does not support audio capture, please upgrade to Android 10 or higher."), ("android_start_service_tip", "Tap [Start Service] or OPEN [Screen Capture] permission to start the screen sharing service."), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("server_not_support", "Not yet supported by the server"), ("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"), ("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"), diff --git a/src/ui/index.tis b/src/ui/index.tis index e718e4380..68787c86f 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -558,8 +558,6 @@ class App: Reactor.Component {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} {!service_stopped && is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} {system_error ? : ""} - {!system_error && handler.is_login_wayland() && !handler.current_is_wayland() ? : ""} - {!system_error && handler.current_is_wayland() ? : ""}
    @@ -746,34 +744,6 @@ class InstallDaemon: Reactor.Component { } } -class FixWayland: Reactor.Component { - function render() { - return
    -
    {translate('Warning')}
    -
    {translate('Login screen using Wayland is not supported')}
    -
    {translate('Help')}
    -
    ; - } - - event click $(#help-me) { - handler.open_url(translate("doc_fix_wayland")); - } -} - -class ModifyDefaultLogin: Reactor.Component { - function render() { - return
    -
    {translate('Warning')}
    -
    {translate('Current Wayland display server is not supported')}
    -
    {translate('Help')}
    -
    ; - } - - event click $(#help-me) { - handler.open_url(translate("doc_fix_wayland")); - } -} - function watch_trust() { // not use TrustMe::update, because it is buggy var trusted = handler.is_process_trusted(false); From 91244ea610d94a328ebdd3fcaac420c6da7426a7 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:40:03 +0800 Subject: [PATCH 264/734] Update cn.rs --- src/lang/cn.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index c028ed36c..8126e0081 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -281,12 +281,12 @@ 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", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), ("android_stop_service_tip", "关闭服务将自动关闭所有已建立的连接。"), - ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓10或更高。"), + ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓 10 或更高。"), ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), ("Account", "账户"), ("Overwrite", "覆盖"), @@ -376,7 +376,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", "其他"), @@ -404,16 +404,16 @@ 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", "添加到地址簿"), ("Group", "小组"), ("Search", "搜索"), - ("Closed manually by web console", "被web控制台手动关闭"), + ("Closed manually by web console", "被 web 控制台手动关闭"), ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), - ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), + ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), @@ -422,9 +422,9 @@ 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对话框。"), + ("wait_accept_uac_tip", "请等待远端用户确认 UAC 对话框。"), ("Elevate successfully", "提权成功"), ("uppercase", "大写字母"), ("lowercase", "小写字母"), From 39515f3ed3e91d02732b5786fc34811a735b52f6 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:48:54 +0800 Subject: [PATCH 265/734] Revert "Remove remnant documentation for wayland fix" --- src/lang/en.rs | 1 + src/ui/index.tis | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/lang/en.rs b/src/lang/en.rs index bacef699c..6eed43a77 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -25,6 +25,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "The current Android version does not support audio capture, please upgrade to Android 10 or higher."), ("android_start_service_tip", "Tap [Start Service] or OPEN [Screen Capture] permission to start the screen sharing service."), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("server_not_support", "Not yet supported by the server"), ("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"), ("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"), diff --git a/src/ui/index.tis b/src/ui/index.tis index 68787c86f..e718e4380 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -558,6 +558,8 @@ class App: Reactor.Component {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} {!service_stopped && is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} {system_error ? : ""} + {!system_error && handler.is_login_wayland() && !handler.current_is_wayland() ? : ""} + {!system_error && handler.current_is_wayland() ? : ""}
    @@ -744,6 +746,34 @@ class InstallDaemon: Reactor.Component { } } +class FixWayland: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('Login screen using Wayland is not supported')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#help-me) { + handler.open_url(translate("doc_fix_wayland")); + } +} + +class ModifyDefaultLogin: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('Current Wayland display server is not supported')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#help-me) { + handler.open_url(translate("doc_fix_wayland")); + } +} + function watch_trust() { // not use TrustMe::update, because it is buggy var trusted = handler.is_process_trusted(false); From f4d030524231c7150044bef2cea45928fd55ac35 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 30 Jan 2023 15:57:27 +0800 Subject: [PATCH 266/734] remove unused tip --- 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/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 - src/ui/index.tis | 2 +- 33 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 9d2938b2d..cd8fba24d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Sortir"), ("Tags", ""), ("Search ID", "Cerca ID"), - ("Current Wayland display server is not supported", "El servidor de visualització actual de Wayland no és compatible"), ("whitelist_sep", ""), ("Add ID", "Afegir ID"), ("Add Tag", "Afegir tag"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 8126e0081..41fa7fc26 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "登出"), ("Tags", "标签"), ("Search ID", "查找ID"), - ("Current Wayland display server is not supported", "不支持 Wayland 显示服务器"), ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), ("Add ID", "增加ID"), ("Add Tag", "增加标签"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 842c47762..5e59a86f1 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Odhlásit se"), ("Tags", "Štítky"), ("Search ID", "Hledat identifikátor"), - ("Current Wayland display server is not supported", "Zobrazovací server Wayland zatím není podporován"), ("whitelist_sep", "Odělováno čárkou, středníkem, mezerou nebo koncem řádku"), ("Add ID", "Přidat identifikátor"), ("Add Tag", "Přidat štítek"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 8e6d622a1..8eddaf0b9 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg ID"), - ("Current Wayland display server is not supported", "Den aktuelle Wayland-Anzege-server understøttes ikke"), ("whitelist_sep", "Adskilt af komma, semikolon, rum eller linjepaus"), ("Add ID", "Tilføj ID"), ("Add Tag", "Tilføj nøgleord"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 11ce96f6b..3418ea9f5 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), - ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt."), ("whitelist_sep", "Getrennt durch Komma, Semikolon, Leerzeichen oder Zeilenumbruch"), ("Add ID", "ID hinzufügen"), ("Add Tag", "Stichwort hinzufügen"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 9086c809a..b034c0394 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Malkonekti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), - ("Current Wayland display server is not supported", "La aktuala bilda servilo Wayland ne estas subtenita"), ("whitelist_sep", "Vi povas uzi komon, punktokomon, spacon aŭ linsalton kiel apartigilo"), ("Add ID", "Aldoni identigilo"), ("Add Tag", "Aldoni etikedo"), diff --git a/src/lang/es.rs b/src/lang/es.rs index e7bf83b25..8f4275d5d 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Salir"), ("Tags", "Tags"), ("Search ID", "Buscar ID"), - ("Current Wayland display server is not supported", "El servidor de visualización actual de Wayland no es compatible"), ("whitelist_sep", "Separados por coma, punto y coma, espacio o nueva línea"), ("Add ID", "Agregar ID"), ("Add Tag", "Agregar tag"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 15ef1b843..316885082 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), - ("Current Wayland display server is not supported", "پشتیبانی نمی شود Wayland سرور نمایش فعلی"), ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), ("Add ID", "افزودن شناسه"), ("Add Tag", "افزودن برچسب"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index aa752f54e..097091e75 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Déconnexion"), ("Tags", "Étiqueter"), ("Search ID", "Rechercher un ID"), - ("Current Wayland display server is not supported", "Le serveur d'affichage Wayland n'est pas pris en charge"), ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"), ("Add ID", "Ajouter un ID"), ("Add Tag", "Ajouter une balise"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 8e73542e5..53f9dca08 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Αποσύνδεση"), ("Tags", "Ετικέτες"), ("Search ID", "Αναζήτηση ID"), - ("Current Wayland display server is not supported", "Ο τρέχων διακομιστής εμφάνισης Wayland δεν υποστηρίζεται"), ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"), ("Add ID", "Προσθήκη αναγνωριστικού ID"), ("Add Tag", "Προσθήκη ετικέτας"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 5ae8e0dca..f86e83012 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Kilépés"), ("Tags", "Tagok"), ("Search ID", "Azonosító keresése..."), - ("Current Wayland display server is not supported", "A Wayland display szerver nem támogatott"), ("whitelist_sep", "A címeket veszővel, pontosvesszővel, szóközzel, vagy új sorral válassza el"), ("Add ID", "Azonosító hozzáadása"), ("Add Tag", "Címke hozzáadása"), diff --git a/src/lang/id.rs b/src/lang/id.rs index f4555fa32..6ae39f108 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Keluar"), ("Tags", "Tag"), ("Search ID", "Cari ID"), - ("Current Wayland display server is not supported", "Server tampilan Wayland saat ini tidak didukung"), ("whitelist_sep", "Dipisahkan dengan koma, titik koma, spasi, atau baris baru"), ("Add ID", "Tambah ID"), ("Add Tag", "Tambah Tag"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 322c324ce..0ec6c52b9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), - ("Current Wayland display server is not supported", "Questo display server Wayland non è supportato"), ("whitelist_sep", "Separati da virgola, punto e virgola, spazio o a capo"), ("Add ID", "Aggiungi ID"), ("Add Tag", "Aggiungi tag"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 65368bfba..8e8a5ed95 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "ログアウト"), ("Tags", "タグ"), ("Search ID", "IDを検索"), - ("Current Wayland display server is not supported", "現在のWaylandディスプレイサーバーはサポートされていません"), ("whitelist_sep", "カンマやセミコロン、空白、改行で区切ってください"), ("Add ID", "IDを追加"), ("Add Tag", "タグを追加"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 01b30adc0..7b56202a0 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), - ("Current Wayland display server is not supported", "현재 Wayland 디스플레이 서버가 지원되지 않습니다"), ("whitelist_sep", "다음 글자로 구분합니다. ',(콤마) ;(세미콜론) 띄어쓰기 혹은 줄바꿈'"), ("Add ID", "ID 추가"), ("Add Tag", "태그 추가"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 48d94c266..dcf62ff10 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Шығу"), ("Tags", "Тақтар"), ("Search ID", "ID Іздеу"), - ("Current Wayland display server is not supported", "Ағымдағы Wayland дисплей серберіне қолдау көрсетілмейді"), ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), ("Add ID", "ID Қосу"), ("Add Tag", "Тақ Қосу"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 467d918b6..085e74d3a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), - ("Current Wayland display server is not supported", "Obecny serwer wyświetlania Wayland nie jest obsługiwany"), ("whitelist_sep", "Oddzielone przecinkiem, średnikiem, spacją lub w nowej linii"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj Tag"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 3b6f02854..aea9acd2a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Procurar ID"), - ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), ("Add ID", "Adicionar ID"), ("Add Tag", "Adicionar Tag"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 9a69d1547..28683c8d5 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Pesquisar ID"), - ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), ("Add ID", "Adicionar ID"), ("Add Tag", "Adicionar Tag"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 148723a5b..3009e9b06 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -218,7 +218,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Deconectare"), ("Tags", "Etichetare"), ("Search ID", "Caută după ID"), - ("Current Wayland display server is not supported", "Serverul de afișaj Wayland nu este acceptat"), ("whitelist_sep", "Poți folosi ca separator virgula, punctul și virgula, spațiul sau linia nouă"), ("Add ID", "Adaugă ID"), ("Add Tag", "Adaugă etichetă"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 5ab0e6af4..7a7445534 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Выйти"), ("Tags", "Метки"), ("Search ID", "Поиск по ID"), - ("Current Wayland display server is not supported", "Текущий сервер отображения Wayland не поддерживается"), ("whitelist_sep", "Раздельно запятой, точкой с запятой, пробелом или новой строкой"), ("Add ID", "Добавить ID"), ("Add Tag", "Добавить ключевое слово"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index b996295f2..2062b57a5 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Odhlásenie"), ("Tags", "Štítky"), ("Search ID", "Hľadať ID"), - ("Current Wayland display server is not supported", "Zobrazovací (display) server Wayland nie je podporovaný"), ("whitelist_sep", "Oddelené čiarkou, bodkočiarkou, medzerou alebo koncom riadku"), ("Add ID", "Pridať ID"), ("Add Tag", "Pridať štítok"), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index cca53c830..1ff78818c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Odjavi"), ("Tags", "Oznake"), ("Search ID", "Išči ID"), - ("Current Wayland display server is not supported", "Trenutni Wayland zaslonski strežnik ni podprt"), ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj oznako"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 3bec54dd8..225652056 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Dalje"), ("Tags", "Tage"), ("Search ID", "Kerko ID"), - ("Current Wayland display server is not supported", "Serveri aktual i ekranit Wayland nuk mbështetet"), ("whitelist_sep", "Të ndara me presje, pikëpresje, hapësira ose rresht të ri"), ("Add ID", "Shto ID"), ("Add Tag", "Shto Tag"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 413d3e165..57c528fdb 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Odjava"), ("Tags", "Oznake"), ("Search ID", "Traži ID"), - ("Current Wayland display server is not supported", "Tekući Wazland server za prikaz nije podržan"), ("whitelist_sep", "Odvojeno zarezima, tačka zarezima, praznim mestima ili novim redovima"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj oznaku"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index baf3c3725..f98d7f005 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Logga ut"), ("Tags", "Taggar"), ("Search ID", "Sök ID"), - ("Current Wayland display server is not supported", "Nuvarande Wayland displayserver stöds inte"), ("whitelist_sep", "Separerat av ett comma, semikolon, mellanslag eller ny linje"), ("Add ID", "Lägg till ID"), ("Add Tag", "Lägg till Tagg"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 1c5305976..358444986 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", ""), ("Tags", ""), ("Search ID", ""), - ("Current Wayland display server is not supported", ""), ("whitelist_sep", ""), ("Add ID", ""), ("Add Tag", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 6fc94ca2a..d35cbdfe7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "ออกจากระบบ"), ("Tags", "แท็ก"), ("Search ID", "ค้นหา ID"), - ("Current Wayland display server is not supported", "เซิร์ฟเวอร์การแสดงผล Wayland ปัจจุบันไม่รองรับ"), ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), ("Add ID", "เพิ่ม ID"), ("Add Tag", "เพิ่มแท็ก"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 4ed9b2213..1e2068fb6 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Çıkış yap"), ("Tags", "Etiketler"), ("Search ID", "ID Arama"), - ("Current Wayland display server is not supported", "Mevcut Wayland görüntüleme sunucusu desteklenmiyor"), ("whitelist_sep", "Virgül, noktalı virgül, boşluk veya yeni satır ile ayrılmış"), ("Add ID", "ID Ekle"), ("Add Tag", "Etiket Ekle"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index cb68254bb..370c9fbed 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), - ("Current Wayland display server is not supported", "目前不支援 Wayland 顯示伺服器"), ("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"), ("Add ID", "新增 ID"), ("Add Tag", "新增標籤"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 78b611ea4..bdba09b5b 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Вийти"), ("Tags", "Ключові слова"), ("Search ID", "Пошук за ID"), - ("Current Wayland display server is not supported", "Поточний графічний сервер Wayland не підтримується"), ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), ("Add ID", "Додати ID"), ("Add Tag", "Додати ключове слово"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 044e2e9e6..840739765 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Đăng xuất"), ("Tags", "Tags"), ("Search ID", "Tìm ID"), - ("Current Wayland display server is not supported", "Máy chủ hình ảnh Wayland hiện không đuợc hỗ trợ"), ("whitelist_sep", "Đuợc cách nhau bởi dấu phẩy, dấu chấm phẩy, dấu cách hay dòng mới"), ("Add ID", "Thêm ID"), ("Add Tag", "Thêm Tag"), diff --git a/src/ui/index.tis b/src/ui/index.tis index e718e4380..ec2e0a748 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -764,7 +764,7 @@ class ModifyDefaultLogin: Reactor.Component { function render() { return
    {translate('Warning')}
    -
    {translate('Current Wayland display server is not supported')}
    +
    {translate('wayland_experiment_tip')}
    {translate('Help')}
    ; } From dec1820694ad2f55e7df6178d5cacc6425ee9a1e Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 30 Jan 2023 17:56:35 +0800 Subject: [PATCH 267/734] opt dialog style Signed-off-by: 21pages --- flutter/lib/common.dart | 92 +++++++++--- .../lib/desktop/widgets/remote_menubar.dart | 8 +- flutter/lib/mobile/widgets/dialog.dart | 138 ++++++++++-------- flutter/lib/models/model.dart | 7 +- src/client/io_loop.rs | 2 +- 5 files changed, 155 insertions(+), 92 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6ee57ef50..ab7728af1 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -613,6 +613,7 @@ class CustomAlertDialog extends StatelessWidget { Future.delayed(Duration.zero, () { if (!scopeNode.hasFocus) scopeNode.requestFocus(); }); + const double padding = 16; return FocusScope( node: scopeNode, autofocus: true, @@ -637,8 +638,8 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, - contentPadding: EdgeInsets.symmetric( - horizontal: contentPadding ?? 25, vertical: 10), + contentPadding: EdgeInsets.fromLTRB( + contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( @@ -648,6 +649,7 @@ class CustomAlertDialog extends StatelessWidget { ), child: content)), actions: actions, + actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding), ), ); } @@ -701,9 +703,8 @@ void msgBox(String id, String type, String title, String text, String link, } dialogManager.show( (setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), - content: - SelectableText(translate(text), style: const TextStyle(fontSize: 15)), + title: null, + content: msgboxContent(type, title, text), actions: buttons, onSubmit: hasOk ? submit : null, onCancel: hasCancel == true ? cancel : null, @@ -712,30 +713,74 @@ void msgBox(String id, String type, String title, String text, String link, ); } -Widget msgBoxButton(String text, void Function() onPressed) { - return ButtonTheme( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - //limits the touch area to the button area - minWidth: 0, - //wraps child's width - height: 0, - child: TextButton( - style: flatButtonStyle, - onPressed: onPressed, - child: - Text(translate(text), style: TextStyle(color: MyTheme.accent)))); +Color? _msgboxColor(String type) { + if (type == "input-password" || type == "custom-os-password") { + return Color(0xFFAD448E); + } + if (type.contains("success")) { + return Color(0xFF32bea6); + } + if (type.contains("error") || type == "re-input-password") { + return Color(0xFFE04F5F); + } + return Color(0xFF2C8CFF); } -Widget _msgBoxTitle(String title) => - Text(translate(title), style: TextStyle(fontSize: 21)); +Widget msgboxIcon(String type) { + IconData? iconData; + if (type.contains("error") || type == "re-input-password") { + iconData = Icons.cancel; + } + if (type.contains("success")) { + iconData = Icons.check_circle; + } + if (type == "wait-uac" || type == "wait-remote-accept-nook") { + iconData = Icons.hourglass_top; + } + if (type == 'on-uac' || type == 'on-foreground-elevated') { + iconData = Icons.admin_panel_settings; + } + if (type == "info") { + iconData = Icons.info; + } + if (iconData != null) { + return Icon(iconData, size: 50, color: _msgboxColor(type)) + .marginOnly(right: 16); + } + + return Offstage(); +} + +// title should be null +Widget msgboxContent(String type, String title, String text) { + return Row( + children: [ + msgboxIcon(type), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate(title), + style: TextStyle(fontSize: 21), + ).marginOnly(bottom: 10), + Text(translate(text), style: const TextStyle(fontSize: 15)), + ], + ), + ), + ], + ); +} void msgBoxCommon(OverlayDialogManager dialogManager, String title, Widget content, List buttons, {bool hasCancel = true}) { dialogManager.dismissAll(); dialogManager.show((setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), + title: Text( + translate(title), + style: TextStyle(fontSize: 21), + ), content: content, actions: buttons, onCancel: hasCancel ? close : null, @@ -1589,7 +1634,8 @@ class ServerConfig { Widget dialogButton(String text, {required VoidCallback? onPressed, bool isOutline = false, - TextStyle? style}) { + TextStyle? style, + ButtonStyle? buttonStyle}) { if (isDesktop) { if (isOutline) { return OutlinedButton( @@ -1598,7 +1644,7 @@ Widget dialogButton(String text, ); } else { return ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0), + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), onPressed: onPressed, child: Text(translate(text), style: style), ); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 07944649c..3598b2fb0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1426,12 +1426,8 @@ void showConfirmSwitchSidesDialog( } return CustomAlertDialog( - title: Text(translate('Switch Sides')), - content: Column( - children: [ - Text(translate('Please confirm if you want to share your desktop?')), - ], - ), + 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), diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 0eb403833..bded6d069 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -9,7 +9,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; void clientClose(String id, OverlayDialogManager dialogManager) { - msgBox(id, '', 'Close', 'Are you sure to close the connection?', '', + msgBox(id, 'info', 'Close', 'Are you sure to close the connection?', '', dialogManager); } @@ -33,8 +33,10 @@ void showRestartRemoteDevice( ]), content: Text( "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + onCancel: close, + onSubmit: () => close(true), actions: [ - dialogButton("Cancel", onPressed: () => close(), isOutline: true), + dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("OK", onPressed: () => close(true)), ], )); @@ -48,6 +50,18 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { var validateLength = false; var validateSame = false; dialogManager.show((setState, close) { + submit() async { + close(); + dialogManager.showLoading(translate("Waiting")); + if (await gFFI.serverModel.setPermanentPassword(p0.text)) { + dialogManager.dismissAll(); + showSuccess(); + } else { + dialogManager.dismissAll(); + showError(); + } + } + return CustomAlertDialog( title: Text(translate('Set your own password')), content: Form( @@ -94,29 +108,17 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { }, ), ])), + onCancel: close, + onSubmit: (validateLength && validateSame) ? submit : null, actions: [ dialogButton( 'Cancel', - onPressed: () { - close(); - }, + onPressed: close, isOutline: true, ), dialogButton( 'OK', - onPressed: (validateLength && validateSame) - ? () async { - close(); - dialogManager.showLoading(translate("Waiting")); - if (await gFFI.serverModel.setPermanentPassword(p0.text)) { - dialogManager.dismissAll(); - showSuccess(); - } else { - dialogManager.dismissAll(); - showError(); - } - } - : null, + onPressed: (validateLength && validateSame) ? submit : null, ), ], ); @@ -205,26 +207,36 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { }); } -void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { - dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate('Wrong Password')), - content: Text(translate('Do you want to enter again?')), - actions: [ - dialogButton( - 'Cancel', - onPressed: () { - close(); - closeConnection(); - }, - isOutline: true, - ), - dialogButton( - 'Retry', - onPressed: () { - enterPasswordDialog(id, dialogManager); - }, - ), - ])); +void wrongPasswordDialog( + String id, OverlayDialogManager dialogManager, type, title, text) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + cancel() { + close(); + closeConnection(); + } + + submit() { + enterPasswordDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + onSubmit: submit, + onCancel: cancel, + actions: [ + dialogButton( + 'Cancel', + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'Retry', + onPressed: submit, + ), + ]); + }); } void showServerSettingsWithValue( @@ -352,13 +364,15 @@ void showServerSettingsWithValue( }); } -void showWaitUacDialog(String id, OverlayDialogManager dialogManager) { +void showWaitUacDialog( + String id, OverlayDialogManager dialogManager, String type) { dialogManager.dismissAll(); dialogManager.show( tag: '$id-wait-uac', (setState, close) => CustomAlertDialog( - title: Text(translate('Wait')), - content: Text(translate('wait_accept_uac_tip')).marginAll(10), + title: null, + content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip') + .marginOnly(bottom: 10), )); } @@ -516,16 +530,6 @@ void showOnBlockDialog( dialogManager.existing('$id-request-elevation')) { return; } - var content = Column(children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}", - textAlign: TextAlign.left, - style: TextStyle(fontWeight: FontWeight.w400), - ).marginSymmetric(vertical: 15), - ), - ]); dialogManager.show(tag: '$id-$type', (setState, close) { void submit() { close(); @@ -533,12 +537,11 @@ void showOnBlockDialog( } return CustomAlertDialog( - title: Text(translate(title)), - content: content, + title: null, + content: msgboxContent(type, title, + "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"), actions: [ - dialogButton('Wait', onPressed: () { - close(); - }, isOutline: true), + dialogButton('Wait', onPressed: close, isOutline: true), dialogButton('Request Elevation', onPressed: submit), ], onSubmit: submit, @@ -556,8 +559,8 @@ void showElevationError(String id, String type, String title, String text, } return CustomAlertDialog( - title: Text(translate(title)), - content: Text(translate(text)), + title: null, + content: msgboxContent(type, title, text), actions: [ dialogButton('Cancel', onPressed: () { close(); @@ -570,6 +573,25 @@ void showElevationError(String id, String type, String title, String text, }); } +void showWaitAcceptDialog(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + onCancel() { + closeConnection(); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + actions: [ + dialogButton('Cancel', onPressed: onCancel, isOutline: true), + ], + onCancel: onCancel, + ); + }); +} + Future validateAsync(String value) async { value = value.trim(); if (value.isEmpty) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 986d93fe8..def9c82bc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -263,19 +263,18 @@ class FfiModel with ChangeNotifier { final text = evt['text']; final link = evt['link']; if (type == 're-input-password') { - wrongPasswordDialog(id, dialogManager); + wrongPasswordDialog(id, dialogManager, type, title, text); } else if (type == 'input-password') { enterPasswordDialog(id, dialogManager); } else if (type == 'restarting') { showMsgBox(id, type, title, text, link, false, dialogManager, hasCancel: false); } else if (type == 'wait-remote-accept-nook') { - msgBoxCommon(dialogManager, title, Text(translate(text)), - [dialogButton("Cancel", onPressed: closeConnection)]); + showWaitAcceptDialog(id, type, title, text, dialogManager); } else if (type == 'on-uac' || type == 'on-foreground-elevated') { showOnBlockDialog(id, type, title, text, dialogManager); } else if (type == 'wait-uac') { - showWaitUacDialog(id, dialogManager); + showWaitUacDialog(id, dialogManager, type); } else if (type == 'elevation-error') { showElevationError(id, type, title, text, dialogManager); } else { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index ff6d6c004..f4ecbded5 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1104,7 +1104,7 @@ impl Remote { Some(misc::Union::PortableServiceRunning(b)) => { if b { self.handler.msgbox( - "custom-nocancel", + "custom-nocancel-success", "Successful", "Elevate successfully", "", From 87de9eb726418d208f77dfc5bb5dcc96c0993655 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 30 Jan 2023 18:30:38 +0800 Subject: [PATCH 268/734] a workaround of issue #2886, following the behavior address input of chrome --- flutter/lib/common/widgets/peer_tab_page.dart | 5 ++++- flutter/lib/desktop/pages/connection_page.dart | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 0c24fe7ea..278f5861c 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -419,7 +419,10 @@ class _PeerSearchBarState extends State { Widget _buildSearchBar() { RxBool focused = false.obs; FocusNode focusNode = FocusNode(); - focusNode.addListener(() => focused.value = focusNode.hasFocus); + focusNode.addListener(() { + focused.value = focusNode.hasFocus; + peerSearchTextController.selection = TextSelection(baseOffset: 0, extentOffset: peerSearchTextController.value.text.length); + }); return Container( width: 120, decoration: BoxDecoration( diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 2dae03250..699cc4495 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -64,6 +64,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); }); windowManager.addListener(this); } From 00a3b04aab8659ef175d9c16715ad7c1be19647f Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 30 Jan 2023 19:38:50 +0800 Subject: [PATCH 269/734] fix theme Signed-off-by: 21pages --- flutter/lib/common.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ab7728af1..cf7de0fa2 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -205,6 +205,9 @@ class MyTheme { splashColor: Colors.transparent, highlightColor: Colors.transparent, splashFactory: isDesktop ? NoSplash.splashFactory : null, + outlinedButtonTheme: OutlinedButtonThemeData( + style: + OutlinedButton.styleFrom(side: BorderSide(color: Colors.white38))), textButtonTheme: isDesktop ? TextButtonThemeData( style: ButtonStyle(splashFactory: NoSplash.splashFactory), @@ -641,13 +644,13 @@ class CustomAlertDialog extends StatelessWidget { contentPadding: EdgeInsets.fromLTRB( contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( - constraints: contentBoxConstraints, - child: Theme( - data: ThemeData( + constraints: contentBoxConstraints, + child: Theme( + data: Theme.of(context).copyWith( inputDecorationTheme: InputDecorationTheme( - isDense: true, contentPadding: EdgeInsets.all(15)), - ), - child: content)), + isDense: true, contentPadding: EdgeInsets.all(15))), + child: content), + ), actions: actions, actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding), ), From d99b0bed0a4f9202e53c472f6dff86e188d92627 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 21:42:58 +0800 Subject: [PATCH 270/734] fix: set edge size to zero when in fullscreen mode --- flutter/lib/desktop/pages/connection_page.dart | 13 +++++++++++++ flutter/lib/desktop/pages/remote_tab_page.dart | 17 +++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 699cc4495..eee4c6a20 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -8,6 +8,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; @@ -92,6 +93,18 @@ class _ConnectionPageState extends State } } + @override + void onWindowEnterFullScreen() { + // Remove edge border by setting the value to zero. + stateGlobal.resizeEdgeSize.value = 0; + } + + @override + void onWindowLeaveFullScreen() { + // Restore edge border to default edge size. + stateGlobal.resizeEdgeSize.value = kWindowEdgeSize; + } + @override void onWindowClose() { super.onWindowClose(); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 83928c3fe..7ceacd539 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -38,8 +38,9 @@ class ConnectionTabPage extends StatefulWidget { } class _ConnectionTabPageState extends State { - final tabController = Get.put(DesktopTabController( - tabType: DesktopTabType.remoteScreen)); + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen)); + final contentKey = UniqueKey(); static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined; @@ -80,7 +81,6 @@ class _ConnectionTabPageState extends State { super.initState(); tabController.onRemoved = (_, id) => onRemoveId(id); - rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( @@ -197,11 +197,12 @@ class _ConnectionTabPageState extends State { ); return Platform.isMacOS ? tabWidget - : SubWindowDragToResizeArea( - child: tabWidget, - resizeEdgeSize: stateGlobal.resizeEdgeSize.value, - windowId: stateGlobal.windowId, - ); + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + )); } // Note: Some dup code to ../widgets/remote_menubar From 0765f7057f3adf7c196f50c630f415878c771096 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 30 Jan 2023 22:12:36 +0800 Subject: [PATCH 271/734] try fix https://github.com/rustdesk/rustdesk/issues/2923 Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 12 +++++++++--- flutter/lib/models/input_model.dart | 7 +++++++ flutter/lib/models/model.dart | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 07944649c..7695bc51c 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -379,9 +379,15 @@ class _RemoteMenubarState extends State { mod_menu.PopupMenuItem( height: _MenubarTheme.height, padding: EdgeInsets.zero, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: rowChildren), + child: Listener( + onPointerHover: (PointerHoverEvent e) => + widget.ffi.inputModel.lastMousePos = e.position, + child: MouseRegion( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: rowChildren), + ), + ), ) ]; }, diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 7356c6ec8..49115cb3f 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -408,6 +408,13 @@ class InputModel { } } + void refreshMousePos() => handleMouse({ + 'x': lastMousePos.dx, + 'y': lastMousePos.dy, + 'buttons': 0, + 'type': _kMouseEventMove, + }); + void handleMouse(Map evt) { double x = evt['x']; double y = max(0.0, evt['y']); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 986d93fe8..1f4fbb8f0 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -244,6 +244,7 @@ class FfiModel with ChangeNotifier { parent.target?.canvasModel.updateViewStyle(); } parent.target?.recordingModel.onSwitchDisplay(); + parent.target?.inputModel.refreshMousePos(); notifyListeners(); } From 55318c2393a44d539d9d92445e05876bf27272ce Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Mon, 30 Jan 2023 17:10:05 +0100 Subject: [PATCH 272/734] Update desktop_tab_page.dart --- flutter/lib/desktop/pages/desktop_tab_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 57c7fe4b8..c1965921c 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -23,7 +23,7 @@ class DesktopTabPage extends StatefulWidget { DesktopTabController tabController = Get.find(); tabController.add(TabInfo( key: kTabLabelSettingPage, - label: kTabLabelSettingPage, + label: translate(kTabLabelSettingPage), selectedIcon: Icons.build_sharp, unselectedIcon: Icons.build_outlined, page: DesktopSettingPage( @@ -46,7 +46,7 @@ class _DesktopTabPageState extends State { RemoteCountState.init(); tabController.add(TabInfo( key: kTabLabelHomePage, - label: kTabLabelHomePage, + label: translate(kTabLabelHomePage), selectedIcon: Icons.home_sharp, unselectedIcon: Icons.home_outlined, closable: false, From 61389bc11fd9b00a8fb742d4239376f8b74ac629 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 30 Jan 2023 21:40:13 +0800 Subject: [PATCH 273/734] adjust quality monitor ui Signed-off-by: 21pages --- flutter/lib/common/widgets/overlay.dart | 72 +++++++++++++------------ src/ui/remote.css | 2 +- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index d9684bace..aaf52fb07 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:provider/provider.dart'; @@ -316,44 +317,49 @@ class _DraggableState extends State { } class QualityMonitor extends StatelessWidget { - static const textStyle = TextStyle(color: MyTheme.grayBg); final QualityMonitorModel qualityMonitorModel; QualityMonitor(this.qualityMonitorModel); + Widget _row(String info, String? value) { + return Row( + children: [ + Expanded( + flex: 8, + child: AutoSizeText(info, + style: TextStyle(color: MyTheme.grayBg), + textAlign: TextAlign.right, + maxLines: 1)), + Spacer(flex: 1), + Expanded( + flex: 8, + child: AutoSizeText(value ?? '', + style: TextStyle(color: MyTheme.grayBg), maxLines: 1)), + ], + ); + } + @override Widget build(BuildContext context) => ChangeNotifierProvider.value( value: qualityMonitorModel, child: Consumer( - builder: (context, qualityMonitorModel, child) => - qualityMonitorModel.show - ? Container( - padding: const EdgeInsets.all(8), - color: MyTheme.canvasColor.withAlpha(120), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: textStyle, - ), - Text( - "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: textStyle, - ), - Text( - "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: textStyle, - ), - Text( - "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: textStyle, - ), - Text( - "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: textStyle, - ), - ], - ), - ) - : const SizedBox.shrink())); + builder: (context, qualityMonitorModel, child) => qualityMonitorModel + .show + ? Container( + constraints: BoxConstraints(maxWidth: 200), + padding: const EdgeInsets.all(8), + color: MyTheme.canvasColor.withAlpha(120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _row("Speed", qualityMonitorModel.data.speed ?? ''), + _row("FPS", qualityMonitorModel.data.fps ?? ''), + _row( + "Delay", "${qualityMonitorModel.data.delay ?? ''}ms"), + _row("Target Bitrate", + "${qualityMonitorModel.data.targetBitrate ?? ''}kb"), + _row("Codec", qualityMonitorModel.data.codecFormat ?? ''), + ], + ), + ) + : const SizedBox.shrink())); } diff --git a/src/ui/remote.css b/src/ui/remote.css index 66c5ce80f..71b2c1682 100644 --- a/src/ui/remote.css +++ b/src/ui/remote.css @@ -16,7 +16,7 @@ div#quality-monitor { padding: 5px; min-width: 150px; color: azure; - border: solid azure; + border: 0.5px solid azure; } video#handler { From fb81f206b7e471f659a07ed0b7a2842e18ec89ad Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 29 Jan 2023 17:36:37 +0800 Subject: [PATCH 274/734] opt flink creation Signed-off-by: 21pages --- src/platform/windows.rs | 10 +++++ src/server/portable_service.rs | 68 ++++++++++++++-------------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index b778283a5..2e0d56eab 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1745,3 +1745,13 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> } return Ok(()); } + +pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg(format!("Everyone:(OI)(CI){}", permission)) + .arg("/T") + .spawn()?; + Ok(()) +} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 748cb39e4..a2f6fb829 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -2,9 +2,7 @@ use core::slice; use hbb_common::{ allow_err, anyhow::anyhow, - bail, - config::Config, - log, + bail, log, message_proto::{KeyEvent, MouseEvent}, protobuf::Message, tokio::{self, sync::mpsc}, @@ -15,6 +13,7 @@ use shared_memory::*; use std::{ mem::size_of, ops::{Deref, DerefMut}, + path::PathBuf, sync::{Arc, Mutex}, time::Duration, }; @@ -25,6 +24,7 @@ use winapi::{ use crate::{ ipc::{self, new_listener, Connection, Data, DataPortableService}, + platform::set_path_permission, video_service::get_current_display, }; @@ -72,7 +72,7 @@ impl DerefMut for SharedMemory { impl SharedMemory { pub fn create(name: &str, size: usize) -> ResultType { - let flink = Self::flink(name.to_string()); + let flink = Self::flink(name.to_string())?; let shmem = match ShmemConf::new() .size(size) .flink(&flink) @@ -91,12 +91,12 @@ impl SharedMemory { } }; log::info!("Create shared memory, size:{}, flink:{}", size, flink); - Self::set_all_perm(&flink); + set_path_permission(&PathBuf::from(flink), "F").ok(); Ok(SharedMemory { inner: shmem }) } pub fn open_existing(name: &str) -> ResultType { - let flink = Self::flink(name.to_string()); + let flink = Self::flink(name.to_string())?; let shmem = match ShmemConf::new().flink(&flink).allow_raw(true).open() { Ok(m) => m, Err(e) => { @@ -116,30 +116,29 @@ impl SharedMemory { } } - fn flink(name: String) -> String { - let mut shmem_flink = format!("shared_memory{}", name); - if cfg!(windows) { - let df = "C:\\ProgramData"; - let df = if std::path::Path::new(df).exists() { - df.to_owned() - } else { - std::env::var("TEMP").unwrap_or("C:\\Windows\\TEMP".to_owned()) - }; - let df = format!("{}\\{}", df, *hbb_common::config::APP_NAME.read().unwrap()); - std::fs::create_dir(&df).ok(); - shmem_flink = format!("{}\\{}", df, shmem_flink); + fn flink(name: String) -> ResultType { + let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); + let mut dir = PathBuf::from(disk); + let dir1 = dir.join("ProgramData"); + let dir2 = std::env::var("TEMP") + .map(|d| PathBuf::from(d)) + .unwrap_or(dir.join("Windows").join("Temp")); + if dir1.exists() { + dir = dir1; + } else if dir2.exists() { + dir = dir2; } else { - shmem_flink = Config::ipc_path("").replace("ipc", "") + &shmem_flink; + bail!("no vaild flink directory"); } - return shmem_flink; - } - - fn set_all_perm(_p: &str) { - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(_p, std::fs::Permissions::from_mode(0o0777)).ok(); + dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); + if !dir.exists() { + std::fs::create_dir(&dir)?; + set_path_permission(&dir, "F").ok(); } + Ok(dir + .join(format!("shared_memory{}", name)) + .to_string_lossy() + .to_string()) } } @@ -451,7 +450,6 @@ pub mod server { // functions called in main process. pub mod client { use hbb_common::anyhow::Context; - use std::path::PathBuf; use super::*; @@ -515,7 +513,7 @@ pub mod client { #[cfg(feature = "flutter")] { if let Some(dir) = PathBuf::from(&exe).parent() { - if !set_dir_permission(&PathBuf::from(dir)) { + if set_path_permission(&PathBuf::from(dir), "RX").is_err() { *SHMEM.lock().unwrap() = None; bail!("Failed to set permission of {:?}", dir); } @@ -533,7 +531,7 @@ pub mod client { let dst = dir.join("rustdesk.exe"); if std::fs::copy(&exe, &dst).is_ok() { if dst.exists() { - if set_dir_permission(&dir) { + if set_path_permission(&dir, "RX").is_ok() { exe = dst.to_string_lossy().to_string(); } } @@ -566,16 +564,6 @@ pub mod client { *QUICK_SUPPORT.lock().unwrap() = v; } - fn set_dir_permission(dir: &PathBuf) -> bool { - // // give Everyone RX permission - std::process::Command::new("icacls") - .arg(dir.as_os_str()) - .arg("/grant") - .arg("Everyone:(OI)(CI)RX") - .arg("/T") - .spawn() - .is_ok() - } pub struct CapturerPortable; impl CapturerPortable { From 74a73b7ffd6008be1d49c67a0642fc1938e4b790 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 31 Jan 2023 17:51:20 +0800 Subject: [PATCH 275/734] add default position for portal streams Signed-off-by: fufesou --- libs/scrap/src/wayland/pipewire.rs | 31 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index fefab9b77..9c0ad9774 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -386,21 +386,22 @@ fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec

    >(), - ) - }) - .next(); - if let Some(v) = v { - if v.len() == 2 { - info.position.0 = v[0] as _; - info.position.1 = v[1] as _; + if let Some(pos) = attributes.get("position") { + let v = pos + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.position.0 = v[0] as _; + info.position.1 = v[1] as _; + } } } Some(info) From c1ae4a6028c8b0b99d0fc0840da0cdad26e5ebd6 Mon Sep 17 00:00:00 2001 From: sjpark Date: Wed, 1 Feb 2023 08:10:57 +0900 Subject: [PATCH 276/734] tray bug fix --- src/core_main.rs | 2 +- src/ui.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 4a2f6164c..8658b736c 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -198,7 +198,7 @@ pub fn core_main() -> Option> { { std::thread::spawn(move || crate::start_server(true)); crate::platform::macos::hide_dock(); - crate::tray::make_tray(); + crate::ui::macos::make_tray(); return None; } #[cfg(target_os = "linux")] diff --git a/src/ui.rs b/src/ui.rs index b8473072d..db1cac074 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -21,7 +21,7 @@ mod cm; #[cfg(feature = "inline")] pub mod inline; #[cfg(target_os = "macos")] -mod macos; +pub mod macos; pub mod remote; #[cfg(target_os = "windows")] pub mod win_privacy; From ec1da900ec6bfa35b7f1302c34871f58604d5e5e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 10:42:02 +0800 Subject: [PATCH 277/734] fix issue #2963: run gen_version no matter debug or release --- libs/hbb_common/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 9e004376c..c9f9e90d7 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -197,9 +197,6 @@ pub fn get_version_from_url(url: &str) -> String { } pub fn gen_version() { - if Ok("release".to_owned()) != std::env::var("PROFILE") { - return; - } println!("cargo:rerun-if-changed=Cargo.toml"); use std::io::prelude::*; let mut file = File::create("./src/version.rs").unwrap(); From 2f26b2a355f896e54f24abba44689ce77ef050b9 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 1 Feb 2023 09:08:54 +0300 Subject: [PATCH 278/734] Update ru.rs --- src/lang/ru.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 7a7445534..22f938ec5 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -41,9 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), ("Privacy Statement", "Заявление о конфиденциальности"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Дата сборки"), + ("Version", "Версия"), + ("Home", "Главная"), ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), @@ -434,7 +434,7 @@ 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", "Закрыто по ожиданию"), ].iter().cloned().collect(); } From 60ff4982ca6337a96cf80630d02aa9a7c76ab120 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 1 Feb 2023 14:03:55 +0800 Subject: [PATCH 279/734] fix: macos location restore incorrectly --- .../desktop/pages/file_manager_tab_page.dart | 3 --- .../desktop/pages/port_forward_tab_page.dart | 3 --- flutter/lib/main.dart | 24 +++++++++---------- flutter/pubspec.yaml | 2 +- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index b2566e267..95bf0b182 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -71,9 +71,6 @@ class _FileManagerTabPageState extends State { reloadCurrentWindow(); } }); - Future.delayed(Duration.zero, () { - restoreWindowPosition(WindowType.FileTransfer, windowId: windowId()); - }); } @override diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index ca354f297..c29ad64b0 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -79,9 +79,6 @@ class _PortForwardTabPageState extends State { reloadCurrentWindow(); } }); - Future.delayed(Duration.zero, () { - restoreWindowPosition(WindowType.PortForward, windowId: windowId()); - }); } @override diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 1ec963f22..4579ef223 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -122,20 +122,20 @@ void runMainApp(bool startService) async { } gFFI.userModel.refreshCurrentUser(); runApp(App()); - // restore the location of the main window before window hide or show - await restoreWindowPosition(WindowType.Main); - // check the startup argument, if we successfully handle the argument, we keep the main window hidden. - if (checkArguments()) { - windowManager.hide(); - } else { - windowManager.show(); - windowManager.focus(); - // move registration of active main window here to prevent async visible check. - rustDeskWinManager.registerActiveWindow(kWindowMainId); - } - // set window option + // Set window option. WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); windowManager.waitUntilReadyToShow(windowOptions, () async { + // Restore the location of the main window before window hide or show. + await restoreWindowPosition(WindowType.Main); + // Check the startup argument, if we successfully handle the argument, we keep the main window hidden. + if (checkArguments()) { + windowManager.hide(); + } else { + windowManager.show(); + windowManager.focus(); + // Move registration of active main window here to prevent from async visible check. + rustDeskWinManager.registerActiveWindow(kWindowMainId); + } windowManager.setOpacity(1); }); windowManager.setTitle(getWindowName()); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0189ad9e4..3d08033bb 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: 057e6eb1bc7dcbcf9dafd1384274a611e4fe7124 + ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.2 window_size: From bf71e38426159e8c85e2d5dbfbaba99675dfa25c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 1 Feb 2023 14:13:53 +0800 Subject: [PATCH 280/734] fix: linux sub-window pos for double-check pos --- flutter/lib/desktop/pages/file_manager_tab_page.dart | 3 +++ flutter/lib/desktop/pages/port_forward_tab_page.dart | 3 +++ flutter/lib/desktop/pages/remote_tab_page.dart | 3 +++ 3 files changed, 9 insertions(+) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 95bf0b182..b2566e267 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -71,6 +71,9 @@ class _FileManagerTabPageState extends State { reloadCurrentWindow(); } }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.FileTransfer, windowId: windowId()); + }); } @override diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index c29ad64b0..ca354f297 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -79,6 +79,9 @@ class _PortForwardTabPageState extends State { reloadCurrentWindow(); } }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.PortForward, windowId: windowId()); + }); } @override diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 7ceacd539..55124fbcc 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -113,6 +113,9 @@ class _ConnectionTabPageState extends State { } _update_remote_count(); }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId()); + }); } @override From cbf0da61956f19478ef87984845aaf73649a5c7a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 1 Feb 2023 16:29:13 +0800 Subject: [PATCH 281/734] feat: add trackpad listener support based on flutter 3.3 --- flutter/lib/common/widgets/remote_input.dart | 2 -- flutter/lib/models/input_model.dart | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 2d0dcacdf..2fb409970 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -64,11 +64,9 @@ class RawPointerMouseRegion extends StatelessWidget { }, onPointerMove: inputModel.onPointMoveImage, onPointerSignal: inputModel.onPointerSignalImage, - /* onPointerPanZoomStart: inputModel.onPointerPanZoomStart, onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, - */ child: MouseRegion( cursor: cursor ?? MouseCursor.defer, onEnter: onEnter, diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 49115cb3f..d2f671cdc 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -310,7 +310,7 @@ class InputModel { } } -/* + int _signOrZero(num x) { if (x == 0) { return 0; @@ -361,7 +361,7 @@ class InputModel { trackpadScrollDistance = Offset.zero; } -*/ + void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); From 5149b90e539a05756b38e14672848bcbcd94ec7c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 17:11:24 +0800 Subject: [PATCH 282/734] fix hide docker (can not call too early) --- flutter/lib/main.dart | 1 + flutter/macos/Podfile.lock | 15 +- .../macos/Runner.xcodeproj/project.pbxproj | 5 +- flutter/pubspec.lock | 671 +++++++++++------- src/core_main.rs | 2 - src/flutter_ffi.rs | 6 + 6 files changed, 440 insertions(+), 260 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 4579ef223..53ae2f5dd 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -88,6 +88,7 @@ Future main(List args) async { debugPrint("--cm started"); desktopType = DesktopType.cm; await windowManager.ensureInitialized(); + bind.mainHideDocker(); runConnectionManagerScreen(args.contains('--hide')); } else if (args.contains('--install')) { runInstallPage(); diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index 8d41945c8..3187c6349 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -13,7 +13,8 @@ PODS: - FMDB/standard (2.7.5) - package_info_plus_macos (0.0.1): - FlutterMacOS - - path_provider_macos (0.0.1): + - path_provider_foundation (0.0.1): + - Flutter - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS @@ -38,7 +39,7 @@ DEPENDENCIES: - flutter_custom_cursor (from `Flutter/ephemeral/.symlinks/plugins/flutter_custom_cursor/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - 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`) - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) @@ -64,8 +65,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral package_info_plus_macos: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos - path_provider_macos: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: @@ -86,14 +87,14 @@ SPEC CHECKSUMS: desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 - FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 - url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index fbf52403c..7a17c3de1 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -279,6 +279,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -429,7 +430,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 15a1a23ac..c193c0651 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,288 +5,328 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + url: "https://pub.dev" source: hosted - version: "50.0.0" + version: "52.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + sha256: "95a1cb2ca1464f44f14769329fbf15987d20ab6c88f8fc5d359bd362be625f29" + url: "https://pub.dev" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 + url: "https://pub.dev" source: hosted version: "2.0.7" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + url: "https://pub.dev" source: hosted - version: "3.3.5" + version: "3.3.6" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" auto_size_text: dependency: "direct main" description: name: auto_size_text - url: "https://pub.dartlang.org" + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" source: hosted version: "3.0.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + sha256: e47660f2178a4392eb72001f9594d3fdcb5efde93e59d2819d61fda499e781c8 + url: "https://pub.dev" source: hosted version: "6.0.2" bot_toast: dependency: "direct main" description: name: bot_toast - url: "https://pub.dartlang.org" + sha256: "19306147033316a7873c5d261b874fca3f341c05e4e1c12be56153ad11187edd" + url: "https://pub.dev" source: hosted version: "4.0.3" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_cli_annotations: dependency: transitive description: name: build_cli_annotations - url: "https://pub.dartlang.org" + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" source: hosted version: "2.1.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + url: "https://pub.dev" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.1.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.3" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.3" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" source: hosted version: "0.3.5" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" source: hosted version: "4.4.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" colorize: dependency: transitive description: name: colorize - url: "https://pub.dartlang.org" + sha256: "584746cd6ba1cba0633b6720f494fe6f9601c4170f0666c1579d2aa2a61071ba" + url: "https://pub.dev" source: hosted version: "3.0.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + sha256: e0c7d60e2fc9f316f5b03f5fe2c0f977d65125345d1a1f77eea02be612e32d0c + url: "https://pub.dev" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b + url: "https://pub.dev" source: hosted version: "0.3.3+2" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" source: hosted version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + sha256: "7ffdeb023fb2c9e194e2147ef8e967d36e4481493178051ceb36d98c62396ddd" + url: "https://pub.dev" source: hosted version: "0.0.15" debounce_throttle: dependency: "direct main" description: name: debounce_throttle - url: "https://pub.dartlang.org" + sha256: c95cf47afda975fc507794a52040a16756fb2f31ad3027d4e691c41862ff5692 + url: "https://pub.dev" source: hosted version: "2.0.0" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + sha256: "0cd056191b701a2b5ba040f2306349e461fafdaa5df4569b2228cdf87b58eced" + url: "https://pub.dev" source: hosted version: "0.3.3" desktop_multi_window: dependency: "direct main" description: path: "." - ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" - resolved-ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" + ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 + resolved-ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -294,98 +334,112 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + sha256: b809c4ed5f7fcdb325ccc70b80ad934677dc4e2aa414bf46859a42bfdfafcbb6 + url: "https://pub.dev" source: hosted version: "4.1.3" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + sha256: "77a8b3c4af06bc46507f89304d9f49dfc64b4ae004b994532ed23b34adeae4b3" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + sha256: "37961762fbd46d3620c7b69ca606671014db55fc1b7a11e696fd90ed2e8fe03d" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: "83fdba24fcf6846d3b10f10dfdc8b6c6d7ada5f8ed21d62ea2909c2dfa043773" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + sha256: "5890f6094df108181c7a29720bc23d0fd6159f17d82787fac093d1fefcaf6325" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "23a2874af0e23ee6e3a2a0ebcecec3a9da13241f2cb93a93a44c8764df123dd7" + url: "https://pub.dev" source: hosted version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + sha256: f3b291b335b7f7c7b721a6f42aeb6209fdfb055ea87980bff68c551b250795ea + url: "https://pub.dev" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + sha256: "44baa799834f4c803921873e7446a2add0f3efa45e101a054b1f0ab9b95f8edc" + url: "https://pub.dev" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + sha256: "2095c626fbbefe70d5a4afc9b1137172a68ee2c276e51c3c1283394485bea8f4" + url: "https://pub.dev" source: hosted version: "1.0.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" ffigen: dependency: "direct dev" description: name: ffigen - url: "https://pub.dartlang.org" + sha256: "42bbfddebacef09c9a4eb2d9ef4049fa6a39edb8622b72ca69200cb6f1e3a6c0" + url: "https://pub.dev" source: hosted version: "7.2.4" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.dartlang.org" + sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 + url: "https://pub.dev" source: hosted - version: "5.2.4" + version: "5.2.5" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + url: "https://pub.dev" source: hosted version: "1.0.1" flutter: @@ -397,42 +451,49 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" + url: "https://pub.dev" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + sha256: "1531680034def621878562ad763079933dabe9f9f5d5add5a094190edc33259b" + url: "https://pub.dev" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" + url: "https://pub.dev" source: hosted version: "3.3.0" flutter_custom_cursor: dependency: "direct main" description: name: flutter_custom_cursor - url: "https://pub.dartlang.org" + sha256: "6c5204cf6a16650355b8aa47a8402e79922c07641390a32021a1069b561909ec" + url: "https://pub.dev" source: hosted - version: "0.0.2" + version: "0.0.3" flutter_improved_scrolling: dependency: "direct main" description: - name: flutter_improved_scrolling - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "62f09545149f320616467c306c8c5f71714a18e6" + resolved-ref: "62f09545149f320616467c306c8c5f71714a18e6" + url: "https://github.com/Kingtous/flutter_improved_scrolling" + source: git version: "0.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_localizations: @@ -444,28 +505,32 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2" + url: "https://pub.dev" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + url: "https://pub.dev" source: hosted version: "2.0.7" flutter_rust_bridge: dependency: "direct main" description: name: flutter_rust_bridge - url: "https://pub.dartlang.org" + sha256: "5aea0f3980dcd314f1890ef0d2392263817899cc15e543734b5d4dbe66b761eb" + url: "https://pub.dev" source: hosted - version: "1.61.1" + version: "1.62.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "6ff9fa12892ae074092de2fa6a9938fb21dbabfdaa2ff57dc697ff912fc8d4b2" + url: "https://pub.dev" source: hosted version: "1.1.6" flutter_web_plugins: @@ -477,427 +542,480 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + sha256: e819441678f1679b719008ff2ff0ef045d66eed9f9ec81166ca0d9b02a187454 + url: "https://pub.dev" source: hosted version: "2.3.2" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" source: hosted version: "2.2.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted version: "3.2.0" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted version: "0.15.1" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" icons_launcher: dependency: "direct dev" description: name: icons_launcher - url: "https://pub.dartlang.org" + sha256: c8e3ae1263822feafaec8a3c666ec84c2143470e1612f5481f1c875024c5f37e + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.6" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.3.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + sha256: f98d76672d309c8b7030c323b3394669e122d52b307d2bbd8d06bd70f5b2aabe + url: "https://pub.dev" source: hosted - version: "0.8.6" + version: "0.8.6+1" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + sha256: b1cbfec0f5aef427a18eb573f5445af8c9c568626bf3388553e40c263d3f7368 + url: "https://pub.dev" source: hosted - version: "0.8.5+4" + version: "0.8.5+5" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + sha256: "7d319fb74955ca46d9bf7011497860e3923bb67feebcf068f489311065863899" + url: "https://pub.dev" source: hosted version: "2.1.10" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + sha256: "39c013200046d14c58b71dc4fa3d00e425fc9c699d589136cd3ca018727c0493" + url: "https://pub.dev" source: hosted - version: "0.8.6+3" + version: "0.8.6+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + sha256: "7cef2f28f4f2fef99180f636c3d446b4ccbafd6ba0fad2adc9a80c4040f656b8" + url: "https://pub.dev" source: hosted version: "2.6.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.8.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8 + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.14" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" + url: "https://pub.dev" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + sha256: f62d7253edc197fe3c88d7c2ddab82d68f555e778d55390ccc3537eca8e8d637 + url: "https://pub.dev" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + sha256: "04b575f44233d30edbb80a94e57cad9107aada334fc02aabb42b6becd13c43fc" + url: "https://pub.dev" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + sha256: a2ad8b4acf4cd479d4a0afa5a74ea3f5b1c7563b77e52cc32b3ee6956d5482a6 + url: "https://pub.dev" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: f7a0c8f1e7e981bc65f8b64137a53fd3c195b18d429fba960babc59a5a1c7ae8 + url: "https://pub.dev" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + sha256: f0829327eb534789e0a16ccac8936a80beed4e2401c4d3a74f3f39094a822d3b + url: "https://pub.dev" source: hosted version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "79524f11c42dd9078b96d797b3cf79c0a2883a50c4920dc43da8562c115089bc" + url: "https://pub.dev" source: hosted version: "2.1.0" password_strength: dependency: "direct main" description: name: password_strength - url: "https://pub.dartlang.org" + sha256: "0e51e3d864e37873a1347e658147f88b66e141ee36c58e19828dc5637961e1ce" + url: "https://pub.dev" source: hosted version: "0.2.0" path: dependency: "direct main" description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.12" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + url: "https://pub.dev" source: hosted version: "2.0.22" - path_provider_ios: + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + url: "https://pub.dev" source: hosted version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + url: "https://pub.dev" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + url: "https://pub.dev" source: hosted version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle - url: "https://pub.dartlang.org" + sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + url: "https://pub.dev" source: hosted version: "3.6.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" source: hosted version: "1.2.1" puppeteer: dependency: transitive description: name: puppeteer - url: "https://pub.dartlang.org" + sha256: "4e235aaf9a338a45c9eb1ee38956e0ba369867bf144d7a27fdaf245409b2b87b" + url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.21.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + sha256: f23b68d893505a424f0bd2e324ebea71ed88465d572d26bb8d2e78a4749591fd + url: "https://pub.dev" source: hosted version: "1.0.1" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted version: "3.2.1" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" source: hosted version: "0.27.7" screen_retriever: @@ -913,42 +1031,48 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.dartlang.org" + sha256: cfca311b6b8d51538ff90e206fbe6ce3b36e7125ea6da4a40eb626c7f9f083b1 + url: "https://pub.dev" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + sha256: d9838037cb554b24b4218b2d07666fbada3478882edefae375ee892b6c820ef3 + url: "https://pub.dev" source: hosted version: "2.0.2" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_static: dependency: transitive description: name: shelf_static - url: "https://pub.dartlang.org" + sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + url: "https://pub.dev" source: hosted version: "1.1.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted version: "1.0.3" simple_observable: dependency: transitive description: name: simple_observable - url: "https://pub.dartlang.org" + sha256: b392795c48f8b5f301b4c8f73e15f56e38fe70f42278c649d8325e859a783301 + url: "https://pub.dev" source: hosted version: "2.0.0" sky_engine: @@ -960,308 +1084,352 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" source: hosted version: "1.2.6" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f" + url: "https://pub.dev" source: hosted - version: "2.0.3+1" + version: "2.2.4+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2+2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + url: "https://pub.dev" source: hosted - version: "3.0.0+3" + version: "3.0.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + sha256: "3814548f25ee11f88d3b1905e2e7c8e47e4a406752f553ed287f6d86a2dcf91d" + url: "https://pub.dev" source: hosted version: "1.4.0" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" uni_links: dependency: "direct main" description: name: uni_links - url: "https://pub.dartlang.org" + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" source: hosted version: "0.5.1" uni_links_desktop: dependency: "direct main" description: name: uni_links_desktop - url: "https://pub.dartlang.org" + sha256: "205484c01890259b56d9271bcf299adf9889e881616c976f13061e29e94bb9f0" + url: "https://pub.dev" source: hosted version: "0.1.4" uni_links_platform_interface: dependency: transitive description: name: uni_links_platform_interface - url: "https://pub.dartlang.org" + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" source: hosted version: "1.0.0" uni_links_web: dependency: transitive description: name: uni_links_web - url: "https://pub.dartlang.org" + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" source: hosted version: "0.1.0" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef" + url: "https://pub.dev" source: hosted version: "2.0.4" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809" + url: "https://pub.dev" source: hosted - version: "6.1.7" + version: "6.1.8" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + url: "https://pub.dev" source: hosted - version: "6.0.22" + version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.0.18" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + url: "https://pub.dev" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" source: hosted version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + sha256: "59f7f31c919c59cbedd37c617317045f5f650dc0eeb568b0b0de9a36472bdb28" + url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + sha256: "984388511230bac63feb53b2911a70e829fe0976b6b2213f5c579c4e0a882db3" + url: "https://pub.dev" source: hosted version: "2.3.10" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + sha256: d9f7a46d6a77680adb03ec05a381025d6e890ebe636637c6c3014cc3926b97e9 + url: "https://pub.dev" source: hosted version: "2.3.8" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + sha256: "42bb75de5e9b79e1f20f1d95f688fac0f95beac4d89c6eb2cd421724d4432dae" + url: "https://pub.dev" source: hosted version: "6.0.1" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + sha256: b649b07b8f8f553bee4a97a0a53d0fe78a70b115eafaf0105b612b32b05ddb99 + url: "https://pub.dev" source: hosted version: "2.0.13" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + url: "https://pub.dev" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db" + url: "https://pub.dev" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" + url: "https://pub.dev" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567" + url: "https://pub.dev" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" win32: dependency: "direct main" description: name: win32 - url: "https://pub.dartlang.org" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" source: hosted version: "3.1.3" win32_registry: dependency: transitive description: name: win32_registry - url: "https://pub.dartlang.org" + sha256: "66e78552f17501aced68fe77425b13156998f1bd3d58f1cd8cd0af2dbe4520e3" + url: "https://pub.dev" source: hosted version: "1.0.2" window_manager: @@ -1286,37 +1454,42 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" yaml_edit: dependency: transitive description: name: yaml_edit - url: "https://pub.dartlang.org" + sha256: "4240d1b19841b8af5786121e4e357735cc2a8ffb19176bff5769d73c34e2a8a5" + url: "https://pub.dev" source: hosted version: "2.0.3" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + sha256: "1913c33844c68b62573741134ef5f987f1e15e331c95ac7dc327afbb9896e9ec" + url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" sdks: - dart: ">=2.17.1 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" diff --git a/src/core_main.rs b/src/core_main.rs index 8658b736c..d5b56bc1f 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -247,8 +247,6 @@ pub fn core_main() -> Option> { #[cfg(feature = "flutter")] crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); - #[cfg(target_os = "macos")] - crate::platform::macos::hide_dock(); } } //_async_logger_holder.map(|x| x.flush()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ebaa160f1..c2ae2b6b0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1241,6 +1241,12 @@ pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) } +pub fn main_hide_docker() -> SyncReturn { + #[cfg(target_os = "macos")] + crate::platform::macos::hide_dock(); + SyncReturn(true) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ From 20003841d080de149ca96a798d99d8719e5d0458 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 17:36:05 +0800 Subject: [PATCH 283/734] bind.mainHideDocker must be put in windowManager.waitUntilReadyToShow --- flutter/lib/main.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 53ae2f5dd..ff7a72124 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -88,7 +88,6 @@ Future main(List args) async { debugPrint("--cm started"); desktopType = DesktopType.cm; await windowManager.ensureInitialized(); - bind.mainHideDocker(); runConnectionManagerScreen(args.contains('--hide')); } else if (args.contains('--install')) { runInstallPage(); @@ -224,6 +223,7 @@ void showCmWindow() { WindowOptions windowOptions = getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); windowManager.waitUntilReadyToShow(windowOptions, () async { + bind.mainHideDocker(); await windowManager.show(); await Future.wait([windowManager.focus(), windowManager.setOpacity(1)]); // ensure initial window size to be changed @@ -237,6 +237,7 @@ void hideCmWindow() { getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); windowManager.setOpacity(0); windowManager.waitUntilReadyToShow(windowOptions, () async { + bind.mainHideDocker(); await windowManager.hide(); }); } From 2e53580caaf069e4e044b03d45331ab6ddfe2ce6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 19:36:36 +0800 Subject: [PATCH 284/734] beautify quality monitor --- flutter/lib/common/widgets/overlay.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index aaf52fb07..4b4172ffd 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -320,20 +320,21 @@ class QualityMonitor extends StatelessWidget { final QualityMonitorModel qualityMonitorModel; QualityMonitor(this.qualityMonitorModel); - Widget _row(String info, String? value) { + Widget _row(String info, String? value, {Color? rightColor}) { return Row( children: [ Expanded( flex: 8, child: AutoSizeText(info, - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: MyTheme.darkGray), textAlign: TextAlign.right, maxLines: 1)), Spacer(flex: 1), Expanded( flex: 8, child: AutoSizeText(value ?? '', - style: TextStyle(color: MyTheme.grayBg), maxLines: 1)), + style: TextStyle(color: rightColor ?? Colors.white), + maxLines: 1)), ], ); } @@ -351,13 +352,15 @@ class QualityMonitor extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _row("Speed", qualityMonitorModel.data.speed ?? ''), - _row("FPS", qualityMonitorModel.data.fps ?? ''), + _row("Speed", qualityMonitorModel.data.speed ?? '-'), + _row("FPS", qualityMonitorModel.data.fps ?? '-'), _row( - "Delay", "${qualityMonitorModel.data.delay ?? ''}ms"), + "Delay", "${qualityMonitorModel.data.delay ?? '-'}ms", + rightColor: Colors.green), _row("Target Bitrate", - "${qualityMonitorModel.data.targetBitrate ?? ''}kb"), - _row("Codec", qualityMonitorModel.data.codecFormat ?? ''), + "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), + _row( + "Codec", qualityMonitorModel.data.codecFormat ?? '-'), ], ), ) From 6f95c38f854ed95536843aeeef84791ef29ae216 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 20:36:14 +0800 Subject: [PATCH 285/734] chore: remove useless code --- src/core_main.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index d5b56bc1f..b34047f86 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -290,10 +290,6 @@ fn import_config(path: &str) { /// If it returns [`Some`], then the process will continue, and flutter gui will be started. #[cfg(feature = "flutter")] fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option> { - args.position(|element| { - return element == "--connect"; - }) - .unwrap(); let peer_id = args.next().unwrap_or("".to_string()); if peer_id.is_empty() { eprintln!("please provide a valid peer id"); From fdfda2a982c68d5d7e889a1d29b6b12f78235f9a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 20:40:09 +0800 Subject: [PATCH 286/734] chore: revert last commit and change unwrap to ? --- src/core_main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core_main.rs b/src/core_main.rs index b34047f86..714502e85 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -290,6 +290,9 @@ fn import_config(path: &str) { /// If it returns [`Some`], then the process will continue, and flutter gui will be started. #[cfg(feature = "flutter")] fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option> { + args.position(|element| { + return element == "--connect"; + })?; let peer_id = args.next().unwrap_or("".to_string()); if peer_id.is_empty() { eprintln!("please provide a valid peer id"); From 68cc667f475ee66c445fe66380c98c4ef9f9d934 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 21:28:26 +0800 Subject: [PATCH 287/734] partially fix issue #2747: text selectable, more top margin of buttons on dialog --- flutter/lib/common.dart | 3 ++- flutter/lib/common/widgets/chat_page.dart | 3 ++- .../lib/desktop/pages/desktop_setting_page.dart | 15 +++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index cf7de0fa2..ee7353c12 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -707,7 +707,8 @@ void msgBox(String id, String type, String title, String text, String link, dialogManager.show( (setState, close) => CustomAlertDialog( title: null, - content: msgboxContent(type, title, text), + content: SelectionArea( + child: msgboxContent(type, title, text).paddingOnly(bottom: 10)), actions: buttons, onSubmit: hasOk ? submit : null, onCancel: hasCancel == true ? cancel : null, diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index 510ce1f73..d1d96199a 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -51,7 +51,7 @@ class ChatPage extends StatelessWidget implements PageShape { return Stack( children: [ LayoutBuilder(builder: (context, constraints) { - return DashChat( + final chat = DashChat( onSend: (chatMsg) { chatModel.send(chatMsg); chatModel.inputNode.requestFocus(); @@ -108,6 +108,7 @@ class ChatPage extends StatelessWidget implements PageShape { borderBottomLeft: 8, )), ); + return SelectionArea(child: chat); }), desktopType == DesktopType.cm || chatModel.currentID == ChatModel.clientModeID diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index df87a0ead..06300cda4 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1113,10 +1113,12 @@ class _AboutState extends State<_About> { const SizedBox( height: 8.0, ), - Text('${translate('Version')}: $version') - .marginSymmetric(vertical: 4.0), - Text('${translate('Build Date')}: $buildDate') - .marginSymmetric(vertical: 4.0), + SelectionArea( + child: Text('${translate('Version')}: $version') + .marginSymmetric(vertical: 4.0)), + SelectionArea( + child: Text('${translate('Build Date')}: $buildDate') + .marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); @@ -1137,7 +1139,8 @@ class _AboutState extends State<_About> { decoration: const BoxDecoration(color: Color(0xFF2c8cff)), padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 8), - child: Row( + child: SelectionArea( + child: Row( children: [ Expanded( child: Column( @@ -1157,7 +1160,7 @@ class _AboutState extends State<_About> { ), ), ], - ), + )), ).marginSymmetric(vertical: 4.0) ], ).marginOnly(left: _kContentHMargin) From a9f2144638db5b1c9736fbc63928b9bec55b7b84 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Wed, 1 Feb 2023 15:42:28 +0100 Subject: [PATCH 288/734] Update es.rs new term added --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 8f4275d5d..3848d1925 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -435,6 +435,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", ""), + ("Closed as expected", "Cerrado como se esperaba"), ].iter().cloned().collect(); } From 8d60bcd51a503a06e3c9e95b716d8be5dbd06a28 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 1 Feb 2023 19:49:41 +0800 Subject: [PATCH 289/734] remove useless empty --switch_uuid Signed-off-by: 21pages --- flutter/lib/utils/multi_window_manager.dart | 9 ++++++--- src/core_main.rs | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 224052bff..550e9ab08 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -43,11 +43,14 @@ class RustDeskMultiWindowManager { Future newRemoteDesktop(String remoteId, {String? switch_uuid}) async { - final msg = jsonEncode({ + var params = { "type": WindowType.RemoteDesktop.index, "id": remoteId, - "switch_uuid": switch_uuid ?? "" - }); + }; + if (switch_uuid != null) { + params['switch_uuid'] = switch_uuid; + } + final msg = jsonEncode(params); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); diff --git a/src/core_main.rs b/src/core_main.rs index 714502e85..89a962f1d 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -304,9 +304,13 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option Date: Wed, 1 Feb 2023 19:56:57 +0800 Subject: [PATCH 290/734] default display settings Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 215 +++++++++++++++++- .../lib/desktop/widgets/remote_menubar.dart | 44 ++-- libs/hbb_common/src/config.rs | 107 ++++++++- src/flutter_ffi.rs | 8 + src/lang/ca.rs | 10 +- src/lang/cn.rs | 8 + src/lang/cs.rs | 8 + src/lang/da.rs | 8 + src/lang/de.rs | 8 + src/lang/eo.rs | 10 +- src/lang/es.rs | 10 +- src/lang/fa.rs | 10 +- src/lang/fr.rs | 10 +- src/lang/gr.rs | 10 +- src/lang/hu.rs | 8 + src/lang/id.rs | 10 +- src/lang/it.rs | 10 +- src/lang/ja.rs | 10 +- src/lang/ko.rs | 10 +- src/lang/kz.rs | 10 +- src/lang/pl.rs | 10 +- src/lang/pt_PT.rs | 10 +- src/lang/ptbr.rs | 10 +- src/lang/ro.rs | 11 + src/lang/ru.rs | 10 +- src/lang/sk.rs | 10 +- src/lang/sl.rs | 10 +- src/lang/sq.rs | 10 +- src/lang/sr.rs | 10 +- src/lang/sv.rs | 10 +- src/lang/template.rs | 10 +- src/lang/th.rs | 10 +- src/lang/tr.rs | 10 +- src/lang/tw.rs | 10 +- src/lang/ua.rs | 10 +- src/lang/vn.rs | 10 +- src/ui_interface.rs | 12 + 37 files changed, 643 insertions(+), 54 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 06300cda4..e4a7e1a25 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -33,6 +33,7 @@ const double _kContentFontSize = 15; const Color _accentColor = MyTheme.accent; const String _kSettingPageControllerTag = 'settingPageController'; const String _kSettingPageIndexTag = 'settingPageIndex'; +const int _kPageCount = 6; class _TabInfo { late final String label; @@ -51,7 +52,7 @@ class DesktopSettingPage extends StatefulWidget { State createState() => _DesktopSettingPageState(); static void switch2page(int page) { - if (page >= 5) return; + if (page >= _kPageCount) return; try { if (Get.isRegistered(tag: _kSettingPageControllerTag)) { DesktopTabPage.onAddSetting(initialPage: page); @@ -75,6 +76,7 @@ class _DesktopSettingPageState extends State _TabInfo('Security', Icons.enhanced_encryption_outlined, Icons.enhanced_encryption), _TabInfo('Network', Icons.link_outlined, Icons.link), + _TabInfo('Display', Icons.desktop_windows_outlined, Icons.desktop_windows), _TabInfo('Account', Icons.person_outline, Icons.person), _TabInfo('About', Icons.info_outline, Icons.info) ]; @@ -88,7 +90,8 @@ class _DesktopSettingPageState extends State @override void initState() { super.initState(); - selectedIndex = (widget.initialPage < 5 ? widget.initialPage : 0).obs; + selectedIndex = + (widget.initialPage < _kPageCount ? widget.initialPage : 0).obs; Get.put(selectedIndex, tag: _kSettingPageIndexTag); controller = PageController(initialPage: widget.initialPage); Get.put(controller, tag: _kSettingPageControllerTag); @@ -130,6 +133,7 @@ class _DesktopSettingPageState extends State _General(), _Safety(), _Network(), + _Display(), _Account(), _About(), ], @@ -1047,6 +1051,213 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } } +class _Display extends StatefulWidget { + const _Display({Key? key}) : super(key: key); + + @override + State<_Display> createState() => _DisplayState(); +} + +class _DisplayState extends State<_Display> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + controller: scrollController, + physics: NeverScrollableScrollPhysics(), + children: [ + viewStyle(context), + scrollStyle(context), + imageQuality(context), + codec(context), + ]).marginOnly(bottom: _kListViewBottomMargin)); + } + + Widget viewStyle(BuildContext context) { + final key = 'view_style'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + return _Card(title: 'Default View Style', children: [ + _Radio(context, + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + label: 'Scale original', + onChanged: onChanged), + _Radio(context, + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + label: 'Scale adaptive', + onChanged: onChanged), + ]); + } + + Widget scrollStyle(BuildContext context) { + final key = 'scroll_style'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + return _Card(title: 'Default Scroll Style', children: [ + _Radio(context, + value: kRemoteScrollStyleAuto, + groupValue: groupValue, + label: 'ScrollAuto', + onChanged: onChanged), + _Radio(context, + value: kRemoteScrollStyleBar, + groupValue: groupValue, + label: 'Scrollbar', + onChanged: onChanged), + ]); + } + + Widget imageQuality(BuildContext context) { + final key = 'image_quality'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + final qualityKey = 'custom_image_quality'; + final qualityValue = + (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? + 50.0) + .obs; + final fpsKey = 'custom-fps'; + final fpsValue = + (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0) + .obs; + return _Card(title: 'Default Image Quality', children: [ + _Radio(context, + value: kRemoteImageQualityBest, + groupValue: groupValue, + label: 'Good image quality', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityBalanced, + groupValue: groupValue, + label: 'Balanced', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityLow, + groupValue: groupValue, + label: 'Optimize reaction time', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityCustom, + groupValue: groupValue, + label: 'Custom', + onChanged: onChanged), + Offstage( + offstage: groupValue != kRemoteImageQualityCustom, + child: Column( + children: [ + Obx(() => Row( + children: [ + Slider( + value: qualityValue.value, + min: 10.0, + max: 100.0, + divisions: 18, + onChanged: (double value) async { + qualityValue.value = value; + await bind.mainSetUserDefaultOption( + key: qualityKey, value: value.toString()); + }, + ), + SizedBox( + width: 40, + child: Text( + '${qualityValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + Obx(() => Row( + children: [ + Slider( + value: fpsValue.value, + min: 10.0, + max: 120.0, + divisions: 22, + onChanged: (double value) async { + fpsValue.value = value; + await bind.mainSetUserDefaultOption( + key: fpsKey, value: value.toString()); + }, + ), + SizedBox( + width: 40, + child: Text( + '${fpsValue.value.round()}', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + ], + ), + ) + ]); + } + + Widget codec(BuildContext context) { + if (!bind.mainHasHwcodec()) { + return Offstage(); + } + final key = 'codec-preference'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + + return _Card(title: 'Default Codec', children: [ + _Radio(context, + value: 'auto', + groupValue: groupValue, + label: 'Auto', + onChanged: onChanged), + _Radio(context, + value: 'vp9', + groupValue: groupValue, + label: 'VP9', + onChanged: onChanged), + _Radio(context, + value: 'h264', + groupValue: groupValue, + label: 'H264', + onChanged: onChanged), + _Radio(context, + value: 'h265', + groupValue: groupValue, + label: 'H265', + onChanged: onChanged), + ]); + } +} + class _Account extends StatefulWidget { const _Account({Key? key}) : super(key: key); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 517dc9750..64d289fcc 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -867,18 +867,24 @@ class _RemoteMenubarState extends State { value: qualitySliderValue.value, min: qualityMinValue, max: qualityMaxValue, - divisions: 90, + divisions: 18, onChanged: (double value) { qualitySliderValue.value = value; debouncerQuality.value = value; }, ), SizedBox( - width: 90, - child: Obx(() => Text( - '${qualitySliderValue.value.round()}% Bitrate', - style: const TextStyle(fontSize: 15), - ))) + 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 @@ -919,20 +925,17 @@ class _RemoteMenubarState extends State { }, ))), SizedBox( - width: 90, - child: Obx(() { - final fps = fpsSliderValue.value.round(); - String text; - if (fps < 100) { - text = '$fps FPS'; - } else { - text = '$fps FPS'; - } - return Text( - text, - style: const TextStyle(fontSize: 15), - ); - })) + 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), + )) ], ), ); @@ -1111,6 +1114,7 @@ class _RemoteMenubarState extends State { )); } } + displayMenu.add(MenuEntryDivider()); /// Show remote cursor if (!widget.ffi.canvasModel.cursorEmbedded) { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 8bea99106..ce4be6119 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -192,7 +192,10 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, - #[serde(default)] + #[serde( + default = "PeerConfig::default_custom_image_quality", + deserialize_with = "PeerConfig::deserialize_custom_image_quality" + )] pub custom_image_quality: Vec, #[serde(default)] pub show_remote_cursor: bool, @@ -961,26 +964,51 @@ impl PeerConfig { serde_field_string!( default_view_style, deserialize_view_style, - "original".to_owned() + UserDefaultConfig::load().get("view_style") ); serde_field_string!( default_scroll_style, deserialize_scroll_style, - "scrollauto".to_owned() + UserDefaultConfig::load().get("scroll_style") ); serde_field_string!( default_image_quality, deserialize_image_quality, - "balanced".to_owned() + UserDefaultConfig::load().get("image_quality") ); + fn default_custom_image_quality() -> Vec { + let f: f64 = UserDefaultConfig::load() + .get("custom_image_quality") + .parse() + .unwrap_or(50.0); + vec![f as _] + } + + fn deserialize_custom_image_quality<'de, D>(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + let v: Vec = de::Deserialize::deserialize(deserializer)?; + if v.len() == 1 && v[0] >= 10 && v[0] <= 100 { + Ok(v) + } else { + Ok(Self::default_custom_image_quality()) + } + } + fn deserialize_options<'de, D>(deserializer: D) -> Result, D::Error> where D: de::Deserializer<'de>, { let mut mp: HashMap = de::Deserialize::deserialize(deserializer)?; - if !mp.contains_key("codec-preference") { - mp.insert("codec-preference".to_owned(), "auto".to_owned()); + let mut key = "codec-preference"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::load().get(key)); + } + key = "custom-fps"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::load().get(key)); } Ok(mp) } @@ -1192,6 +1220,73 @@ impl HwCodecConfig { } } +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct UserDefaultConfig { + #[serde(default)] + options: HashMap, +} + +impl UserDefaultConfig { + pub fn load() -> UserDefaultConfig { + Config::load_::("_default") + } + + #[inline] + fn store(&self) { + Config::store_(self, "_default"); + } + + pub fn get(&self, key: &str) -> String { + match key { + "view_style" => self.get_string(key, "original", vec!["adaptive"]), + "scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]), + "image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]), + "codec-preference" => self.get_string(key, "auto", vec!["vp9", "h264", "h265"]), + "custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 100.0), + "custom-fps" => self.get_double_string(key, 30.0, 10.0, 120.0), + _ => self + .options + .get(key) + .map(|v| v.to_string()) + .unwrap_or_default(), + } + } + + pub fn set(&mut self, key: String, value: String) { + self.options.insert(key, value); + self.store(); + } + + #[inline] + fn get_string(&self, key: &str, default: &str, others: Vec<&str>) -> String { + match self.options.get(key) { + Some(option) => { + if others.contains(&option.as_str()) { + option.to_owned() + } else { + default.to_owned() + } + } + None => default.to_owned(), + } + } + + #[inline] + fn get_double_string(&self, key: &str, default: f64, min: f64, max: f64) -> String { + match self.options.get(key) { + Some(option) => { + let v: f64 = option.parse().unwrap_or(default); + if v >= min && v <= max { + v.to_string() + } else { + default.to_string() + } + } + None => default.to_string(), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c2ae2b6b0..d40c66d19 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -791,6 +791,14 @@ pub fn main_default_video_save_directory() -> String { default_video_save_directory() } +pub fn main_set_user_default_option(key: String, value: String) { + set_user_default_option(key, value); +} + +pub fn main_get_user_default_option(key: String) -> SyncReturn { + SyncReturn(get_user_default_option(key)) +} + pub fn session_add_port_forward( id: String, local_port: i32, diff --git a/src/lang/ca.rs b/src/lang/ca.rs index cd8fba24d..ac3dba290 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Silenciar"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Silenciar"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), ("Hardware Codec", "Còdec de hardware"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 41fa7fc26..5f03ba759 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "反转访问方向"), ("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", "帧率"), + ("Auto", "自动"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 5e59a86f1..43f3b423a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 8eddaf0b9..5f9e49265 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 3418ea9f5..a683ae44d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index b034c0394..7f92a9b19 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Pri"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Muta"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Muta"), ("Audio Input", "Aŭdia enigo"), ("Enhancements", ""), ("Hardware Codec", ""), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3848d1925..505149759 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Acerca de"), ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), ("Privacy Statement", "Declaración de privacidad"), + ("Mute", "Silenciar"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), ("Hardware Codec", "Códec de hardware"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 316885082..7e1264939 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "درباره"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "بستن صدا"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "بستن صدا"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), ("Hardware Codec", "کدک سخت افزاری"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "طرفین را عوض کنید"), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 097091e75..9b50c8db7 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "À propos de"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), ("Privacy Statement", "Déclaration de confidentialité"), + ("Mute", "Muet"), ("Build Date", "Date de compilation"), ("Version", "Version"), ("Home", "Accueil"), - ("Mute", "Muet"), ("Audio Input", "Entrée audio"), ("Enhancements", "Améliorations"), ("Hardware Codec", "Transcodage matériel"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 53f9dca08..82e90a117 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Πληροφορίες"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Σίγαση"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Σίγαση"), ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), ("Hardware Codec", "Κωδικοποιητής υλικού"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index f86e83012..f1b231d3c 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 6ae39f108..e7b3c2cce 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Tentang"), ("Slogan_tip", ""), ("Privacy Statement", "Pernyataan Privasi"), + ("Mute", "Bisukan"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Bisukan"), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), ("Hardware Codec", "Codec Perangkat Keras"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 0ec6c52b9..ec7e07312 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), + ("Mute", "Silenzia"), ("Build Date", "Data della build"), ("Version", "Versione"), ("Home", "Home"), - ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), ("Hardware Codec", "Codifica Hardware"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 8e8a5ed95..a65f3d56a 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "情報"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "ミュート"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "ミュート"), ("Audio Input", "音声入力デバイス"), ("Enhancements", "追加機能"), ("Hardware Codec", "ハードウェア コーデック"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7b56202a0..8f7167df4 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "정보"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "음소거"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "음소거"), ("Audio Input", "오디오 입력"), ("Enhancements", ""), ("Hardware Codec", "하드웨어 코덱"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index dcf62ff10..1651beb92 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Туралы"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Дыбыссыздандыру"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Дыбыссыздандыру"), ("Audio Input", "Аудио Еңгізу"), ("Enhancements", "Жақсартулар"), ("Hardware Codec", "Hardware Codec"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 085e74d3a..0b0c454c0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O aplikacji"), ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), ("Privacy Statement", "Oświadczenie o ochronie prywatności"), + ("Mute", "Wycisz"), ("Build Date", "Zbudowano"), ("Version", "Wersja"), ("Home", "Pulpit"), - ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), ("Hardware Codec", "Kodek sprzętowy"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "Zmień 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", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index aea9acd2a..d327011fe 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Silenciar"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Silenciar"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), ("Hardware Codec", ""), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 28683c8d5..a442b5858 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Desativar som"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), ("Hardware Codec", "Codec de hardware"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 3009e9b06..b90a21cea 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Fără sunet"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Intrare audio"), ("Enhancements", "Îmbunătățiri"), ("Hardware Codec", "Codec hardware"), @@ -433,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 22f938ec5..f92815137 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), ("Privacy Statement", "Заявление о конфиденциальности"), + ("Mute", "Отключить звук"), ("Build Date", "Дата сборки"), ("Version", "Версия"), ("Home", "Главная"), - ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), ("Hardware Codec", "Аппаратный кодек"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "Переключить стороны"), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 2062b57a5..a6b5b7b4f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O RustDesk"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Stíšiť"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Stíšiť"), ("Audio Input", "Zvukový vstup"), ("Enhancements", ""), ("Hardware Codec", ""), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1ff78818c..1cabf9bbc 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Izklopi zvok"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Izklopi zvok"), ("Audio Input", "Avdio vhod"), ("Enhancements", "Izboljšave"), ("Hardware Codec", "Strojni kodek"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 225652056..6bfdc8230 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Rreth"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Pa zë"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Pa zë"), ("Audio Input", "Inputi zërit"), ("Enhancements", "Përmirësimet"), ("Hardware Codec", "Kodeku Harduerik"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 57c528fdb..cfdb3712b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Utišaj"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Utišaj"), ("Audio Input", "Audio ulaz"), ("Enhancements", "Proširenja"), ("Hardware Codec", "Hardverski kodek"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index f98d7f005..5d25b6a14 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Om"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Tyst"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Tyst"), ("Audio Input", "Ljud input"), ("Enhancements", "Förbättringar"), ("Hardware Codec", "Hårdvarucodec"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 358444986..0e77eca0d 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", ""), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", ""), ("Audio Input", ""), ("Enhancements", ""), ("Hardware Codec", ""), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index d35cbdfe7..da4b7fba5 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "เกี่ยวกับ"), ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Mute", "ปิดเสียง"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "ปิดเสียง"), ("Audio Input", "ออดิโออินพุท"), ("Enhancements", "การปรับปรุง"), ("Hardware Codec", "ฮาร์ดแวร์ codec"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1e2068fb6..717072bfb 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Hakkında"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Sustur"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Sustur"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), ("Hardware Codec", "Donanımsal Codec"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 370c9fbed..0076a7a81 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "關於"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "靜音"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "靜音"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), ("Hardware Codec", "硬件編解碼"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", "幀率"), + ("Auto", "自動"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index bdba09b5b..980febc97 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), + ("Mute", "Вимкнути звук"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Вимкнути звук"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), ("Hardware Codec", "Апаратний кодек"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 840739765..8785acfc3 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "About"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Tắt tiếng"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Tắt tiếng"), ("Audio Input", "Đầu vào âm thanh"), ("Enhancements", "Các tiện itchs"), ("Hardware Codec", "Codec phần cứng"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("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", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 4e0fd7744..d357c9cef 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -917,6 +917,18 @@ pub fn account_auth_result() -> String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } +#[cfg(feature = "flutter")] +pub fn set_user_default_option(key: String, value: String) { + use hbb_common::config::UserDefaultConfig; + UserDefaultConfig::load().set(key, value); +} + +#[cfg(feature = "flutter")] +pub fn get_user_default_option(key: String) -> String { + use hbb_common::config::UserDefaultConfig; + UserDefaultConfig::load().get(&key) +} + // notice: avoiding create ipc connection repeatedly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] From 92145eeb717f1eba07c7298c4bca13e37aad6857 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 2 Feb 2023 09:39:14 +0800 Subject: [PATCH 291/734] other bool default display settings Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 34 ++++++++ libs/hbb_common/src/config.rs | 80 +++++++++++++++---- src/client.rs | 40 +++++----- src/client/io_loop.rs | 14 ++-- 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/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 + 36 files changed, 159 insertions(+), 41 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index e4a7e1a25..4b6cf2a62 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1072,6 +1072,7 @@ class _DisplayState extends State<_Display> { scrollStyle(context), imageQuality(context), codec(context), + other(context), ]).marginOnly(bottom: _kListViewBottomMargin)); } @@ -1256,6 +1257,39 @@ class _DisplayState extends State<_Display> { onChanged: onChanged), ]); } + + Widget otherRow(String label, String key) { + final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; + onChanged(bool b) async { + await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : ''); + setState(() {}); + } + + return GestureDetector( + child: Row( + children: [ + Checkbox(value: value, onChanged: (_) => onChanged(!value)) + .marginOnly(right: 5), + Expanded( + child: Text(translate(label)), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () => onChanged(!value)); + } + + Widget other(BuildContext context) { + return _Card(title: 'Other Default Options', children: [ + otherRow('Show remote cursor', 'show_remote_cursor'), + otherRow('Zoom cursor', 'zoom-cursor'), + otherRow('Show quality monitor', 'show_quality_monitor'), + otherRow('Mute', 'disable_audio'), + otherRow('Allow file copy and paste', 'enable_file_transfer'), + otherRow('Disable clipboard', 'disable_clipboard'), + otherRow('Lock after session end', 'lock_after_session_end'), + otherRow('Privacy mode', 'privacy_mode'), + ]); + } } class _Account extends StatefulWidget { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index ce4be6119..71dd9a5c6 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -115,6 +115,26 @@ macro_rules! serde_field_string { }; } +macro_rules! serde_field_bool { + ($struct_name: ident, $field_name: literal, $func: ident) => { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct $struct_name { + #[serde(rename = $field_name)] + pub v: bool, + } + impl Default for $struct_name { + fn default() -> Self { + Self { v: Self::$func() } + } + } + impl $struct_name { + pub fn $func() -> bool { + UserDefaultConfig::load().get($field_name) == "Y" + } + } + }; +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum NetworkType { Direct, @@ -197,24 +217,24 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_custom_image_quality" )] pub custom_image_quality: Vec, - #[serde(default)] - pub show_remote_cursor: bool, - #[serde(default)] - pub lock_after_session_end: bool, - #[serde(default)] - pub privacy_mode: bool, + #[serde(flatten)] + pub show_remote_cursor: ShowRemoteCursor, + #[serde(flatten)] + pub lock_after_session_end: LockAfterSessionEnd, + #[serde(flatten)] + pub privacy_mode: PrivacyMode, #[serde(default)] pub port_forwards: Vec<(i32, String, i32)>, #[serde(default)] pub direct_failures: i32, - #[serde(default)] - pub disable_audio: bool, - #[serde(default)] - pub disable_clipboard: bool, - #[serde(default)] - pub enable_file_transfer: bool, - #[serde(default)] - pub show_quality_monitor: bool, + #[serde(flatten)] + pub disable_audio: DisableAudio, + #[serde(flatten)] + pub disable_clipboard: DisableClipboard, + #[serde(flatten)] + pub enable_file_transfer: EnableFileTransfer, + #[serde(flatten)] + pub show_quality_monitor: ShowQualityMonitor, #[serde(default)] pub keyboard_mode: String, @@ -1010,10 +1030,42 @@ impl PeerConfig { if !mp.contains_key(key) { mp.insert(key.to_owned(), UserDefaultConfig::load().get(key)); } + key = "zoom-cursor"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::load().get(key)); + } Ok(mp) } } +serde_field_bool!( + ShowRemoteCursor, + "show_remote_cursor", + default_show_remote_cursor +); +serde_field_bool!( + ShowQualityMonitor, + "show_quality_monitor", + default_show_quality_monitor +); +serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio); +serde_field_bool!( + EnableFileTransfer, + "enable_file_transfer", + default_enable_file_transfer +); +serde_field_bool!( + DisableClipboard, + "disable_clipboard", + default_disable_clipboard +); +serde_field_bool!( + LockAfterSessionEnd, + "lock_after_session_end", + default_lock_after_session_end +); +serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode); + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LocalConfig { #[serde(default)] diff --git a/src/client.rs b/src/client.rs index a6df6dbec..fb42ce840 100644 --- a/src/client.rs +++ b/src/client.rs @@ -956,7 +956,7 @@ impl LoginConfigHandler { /// Check if the client should auto login. /// Return password if the client should auto login, otherwise return empty string. pub fn should_auto_login(&self) -> String { - let l = self.lock_after_session_end; + let l = self.lock_after_session_end.v; let a = !self.get_option("auto-login").is_empty(); let p = self.get_option("os-password"); if !p.is_empty() && l && a { @@ -1063,32 +1063,32 @@ impl LoginConfigHandler { let mut option = OptionMessage::default(); let mut config = self.load_config(); if name == "show-remote-cursor" { - config.show_remote_cursor = !config.show_remote_cursor; - option.show_remote_cursor = (if config.show_remote_cursor { + config.show_remote_cursor.v = !config.show_remote_cursor.v; + option.show_remote_cursor = (if config.show_remote_cursor.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "disable-audio" { - config.disable_audio = !config.disable_audio; - option.disable_audio = (if config.disable_audio { + config.disable_audio.v = !config.disable_audio.v; + option.disable_audio = (if config.disable_audio.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "disable-clipboard" { - config.disable_clipboard = !config.disable_clipboard; - option.disable_clipboard = (if config.disable_clipboard { + config.disable_clipboard.v = !config.disable_clipboard.v; + option.disable_clipboard = (if config.disable_clipboard.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "lock-after-session-end" { - config.lock_after_session_end = !config.lock_after_session_end; - option.lock_after_session_end = (if config.lock_after_session_end { + config.lock_after_session_end.v = !config.lock_after_session_end.v; + option.lock_after_session_end = (if config.lock_after_session_end.v { BoolOption::Yes } else { BoolOption::No @@ -1096,15 +1096,15 @@ impl LoginConfigHandler { .into(); } else if name == "privacy-mode" { // try toggle privacy mode - option.privacy_mode = (if config.privacy_mode { + option.privacy_mode = (if config.privacy_mode.v { BoolOption::No } else { BoolOption::Yes }) .into(); } else if name == "enable-file-transfer" { - config.enable_file_transfer = !config.enable_file_transfer; - option.enable_file_transfer = (if config.enable_file_transfer { + config.enable_file_transfer.v = !config.enable_file_transfer.v; + option.enable_file_transfer = (if config.enable_file_transfer.v { BoolOption::Yes } else { BoolOption::No @@ -1115,7 +1115,7 @@ impl LoginConfigHandler { } else if name == "unblock-input" { option.block_input = BoolOption::No.into(); } else if name == "show-quality-monitor" { - config.show_quality_monitor = !config.show_quality_monitor; + config.show_quality_monitor.v = !config.show_quality_monitor.v; } else { let v = self.options.get(&name).is_some(); if v { @@ -1252,19 +1252,19 @@ impl LoginConfigHandler { /// * `name` - The name of the toggle option. pub fn get_toggle_option(&self, name: &str) -> bool { if name == "show-remote-cursor" { - self.config.show_remote_cursor + self.config.show_remote_cursor.v } else if name == "lock-after-session-end" { - self.config.lock_after_session_end + self.config.lock_after_session_end.v } else if name == "privacy-mode" { - self.config.privacy_mode + self.config.privacy_mode.v } else if name == "enable-file-transfer" { - self.config.enable_file_transfer + self.config.enable_file_transfer.v } else if name == "disable-audio" { - self.config.disable_audio + self.config.disable_audio.v } else if name == "disable-clipboard" { - self.config.disable_clipboard + self.config.disable_clipboard.v } else if name == "show-quality-monitor" { - self.config.show_quality_monitor + self.config.show_quality_monitor.v } else { !self.get_option(name).is_empty() } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f4ecbded5..0178fe9e8 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -277,7 +277,7 @@ impl Remote { } if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || lc.read().unwrap().disable_clipboard + || lc.read().unwrap().disable_clipboard.v { continue; } @@ -778,7 +778,7 @@ impl Remote { || self.handler.is_port_forward() || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || self.handler.lc.read().unwrap().disable_clipboard) + || self.handler.lc.read().unwrap().disable_clipboard.v) { let txt = self.old_clipboard.lock().unwrap().clone(); if !txt.is_empty() { @@ -808,7 +808,7 @@ impl Remote { self.handler.set_cursor_position(cp); } Some(message::Union::Clipboard(cb)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { + if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(cb, Some(&self.old_clipboard)); #[cfg(any(target_os = "android", target_os = "ios"))] @@ -1121,7 +1121,7 @@ impl Remote { self.handler.handle_test_delay(t, peer).await; } Some(message::Union::AudioFrame(frame)) => { - if !self.handler.lc.read().unwrap().disable_audio { + if !self.handler.lc.read().unwrap().disable_audio.v { self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); } } @@ -1204,7 +1204,7 @@ impl Remote { #[inline(always)] fn update_privacy_mode(&mut self, on: bool) { let mut config = self.handler.load_config(); - config.privacy_mode = on; + config.privacy_mode.v = on; self.handler.save_config(config); self.handler.update_privacy_mode(); @@ -1278,14 +1278,14 @@ impl Remote { #[cfg(windows)] { let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) - && self.handler.lc.read().unwrap().enable_file_transfer; + && self.handler.lc.read().unwrap().enable_file_transfer.v; ContextSend::enable(enabled); } } #[cfg(windows)] fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { - if !self.handler.lc.read().unwrap().disable_clipboard { + if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { if self.client_conn_id diff --git a/src/lang/ca.rs b/src/lang/ca.rs index ac3dba290..f2210f971 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5f03ba759..00d62946f 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", "波特率"), ("FPS", "帧率"), ("Auto", "自动"), + ("Other Default Options", "其它默认选项"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 43f3b423a..453ecefb3 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 5f9e49265..dcaeb3eaa 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a683ae44d..5b68c0e7a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 7f92a9b19..0c7f13d7e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 505149759..6f866845c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 7e1264939..72cde49f9 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9b50c8db7..19b932d2f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 82e90a117..bc25ab6c6 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index f1b231d3c..49ce8f140 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index e7b3c2cce..0fa6e0293 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index ec7e07312..6edd4a461 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a65f3d56a..35e20d7fd 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 8f7167df4..d03b07992 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 1651beb92..2006c67d1 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 0b0c454c0..daf4a7846 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d327011fe..64e5e9315 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a442b5858..0f64ae67f 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b90a21cea..7e209dff8 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f92815137..7ec6c1554 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a6b5b7b4f..a703c0799 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1cabf9bbc..16c948ceb 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6bfdc8230..285a51732 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index cfdb3712b..dd943e0e6 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 5d25b6a14..3050ff635 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 0e77eca0d..7572da9de 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index da4b7fba5..535e4e772 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 717072bfb..80b384c6c 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0076a7a81..f5d9539d8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", "波特率"), ("FPS", "幀率"), ("Auto", "自動"), + ("Other Default Options", "其它默認選項"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 980febc97..37a7d6bcd 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8785acfc3..d78f5aa7b 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } From 6119e040067530a32b3dc70bac6b5eb9ae4941f6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 2 Feb 2023 13:57:20 +0800 Subject: [PATCH 292/734] fix: synchronize macOS window theme on flutter theme changed. --- flutter/lib/common.dart | 20 +++++++++++ .../lib/desktop/widgets/refresh_wrapper.dart | 4 +++ flutter/lib/main.dart | 4 +++ flutter/lib/utils/platform_channel.dart | 34 +++++++++++++++++++ flutter/macos/Runner/MainFlutterWindow.swift | 31 +++++++++++++++++ flutter/pubspec.yaml | 2 +- 6 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 flutter/lib/utils/platform_channel.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ee7353c12..2a4441d36 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -9,6 +9,7 @@ import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:win32/win32.dart' as win32; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -235,6 +236,12 @@ class MyTheme { } bind.mainChangeTheme(dark: mode.toShortString()); } + // Synchronize the window theme of the system. + if (Platform.isMacOS) { + final isDark = mode == ThemeMode.dark; + RdPlatformChannel.instance.changeSystemWindowTheme( + isDark ? SystemWindowTheme.dark : SystemWindowTheme.light); + } } static ThemeMode currentThemeMode() { @@ -1686,3 +1693,16 @@ String getWindowName({WindowType? overrideType}) { String getWindowNameWithId(String id, {WindowType? overrideType}) { return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; } + +void updateSystemWindowTheme() { + // Set system window theme for macOS + final userPreference = MyTheme.getThemeModePreference(); + if (userPreference != ThemeMode.system) { + if (Platform.isMacOS) { + RdPlatformChannel.instance.changeSystemWindowTheme( + userPreference == ThemeMode.light + ? SystemWindowTheme.light + : SystemWindowTheme.dark); + } + } +} \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/refresh_wrapper.dart b/flutter/lib/desktop/widgets/refresh_wrapper.dart index 60e816044..b4ea14d01 100644 --- a/flutter/lib/desktop/widgets/refresh_wrapper.dart +++ b/flutter/lib/desktop/widgets/refresh_wrapper.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/main.dart'; import 'package:get/get.dart'; class RefreshWrapper extends StatefulWidget { final Widget Function(BuildContext context) builder; + const RefreshWrapper({super.key, required this.builder}); @override @@ -30,6 +32,8 @@ class RefreshWrapperState extends State { if (Get.context != null) { (context as Element).visitChildren(_rebuildElement); } + // Synchronize the window theme of the system. + updateSystemWindowTheme(); setState(() {}); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index ff7a72124..5b1e0c37c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -108,6 +108,8 @@ Future initEnv(String appType) async { await initGlobalFFI(); // await Firebase.initializeApp(); _registerEventHandler(); + // Update the system theme. + updateSystemWindowTheme(); } void runMainApp(bool startService) async { @@ -327,6 +329,8 @@ class _AppState extends State { to = ThemeMode.light; } Get.changeThemeMode(to); + // Synchronize the window theme of the system. + updateSystemWindowTheme(); if (desktopType == DesktopType.main) { bind.mainChangeTheme(dark: to.toShortString()); } diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart new file mode 100644 index 000000000..21f08f53f --- /dev/null +++ b/flutter/lib/utils/platform_channel.dart @@ -0,0 +1,34 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/main.dart'; + +enum SystemWindowTheme { light, dark } + +/// The platform channel for RustDesk +class RdPlatformChannel { + RdPlatformChannel._(); + + static final RdPlatformChannel _windowUtil = RdPlatformChannel._(); + + static RdPlatformChannel get instance => _windowUtil; + + final MethodChannel _osxMethodChannel = + MethodChannel("org.rustdesk.rustdesk/macos"); + final MethodChannel _winMethodChannel = + MethodChannel("org.rustdesk.rustdesk/windows"); + final MethodChannel _linuxMethodChannel = + MethodChannel("org.rustdesk.rustdesk/linux"); + + /// Change the theme of the system window + Future changeSystemWindowTheme(SystemWindowTheme theme) { + assert(Platform.isMacOS); + if (kDebugMode) { + print( + "[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}"); + } + return _osxMethodChannel + .invokeMethod("setWindowTheme", {"themeName": theme.name}); + } +} diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 108f5a5f8..cea1e94bb 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -27,12 +27,16 @@ class MainFlutterWindow: NSWindow { let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) + // register self method handler + let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin") + setMethodHandler(registrar: registrar) RegisterGeneratedPlugins(registry: flutterViewController) FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in // Register the plugin which you want access from other isolate. // DesktopLifecyclePlugin.register(with: controller.registrar(forPlugin: "DesktopLifecyclePlugin")) + self.setMethodHandler(registrar: controller.registrar(forPlugin: "RustDeskPlugin")) DesktopDropPlugin.register(with: controller.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: controller.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FlutterCustomCursorPlugin.register(with: controller.registrar(forPlugin: "FlutterCustomCursorPlugin")) @@ -53,4 +57,31 @@ class MainFlutterWindow: NSWindow { super.order(place, relativeTo: otherWin) hiddenWindowAtLaunch() } + + /// Override window theme. + public func setWindowInterfaceMode(window: NSWindow, themeName: String) { + window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua) + } + + public func setMethodHandler(registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/macos", binaryMessenger: registrar.messenger) + channel.setMethodCallHandler({ + (call, result) -> Void in + switch call.method { + case "setWindowTheme": + let arg = call.arguments as! [String: Any] + let themeName = arg["themeName"] as? String + guard let window = registrar.view?.window else { + result(nil) + return + } + self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light") + result(nil) + break; + default: + result(FlutterMethodNotImplemented) + } + }) + } } + diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 3d08033bb..95449e611 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,7 +61,7 @@ dependencies: url: https://github.com/Kingtous/rustdesk_desktop_multi_window ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 freezed_annotation: ^2.0.3 - flutter_custom_cursor: ^0.0.2 + flutter_custom_cursor: ^0.0.4 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git From 205f37cd56a715b07c2379a32171f32349b21fdf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 2 Feb 2023 14:03:50 +0800 Subject: [PATCH 293/734] opt: shrink unnecessary theme code --- flutter/lib/common.dart | 22 +++++++++------------- flutter/lib/utils/platform_channel.dart | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2a4441d36..a2623ff15 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -225,22 +225,18 @@ class MyTheme { return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); } - static void changeDarkMode(ThemeMode mode) { + static void changeDarkMode(ThemeMode mode) async { Get.changeThemeMode(mode); if (desktopType == DesktopType.main) { if (mode == ThemeMode.system) { - bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); + await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); } else { - bind.mainSetLocalOption( + await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - bind.mainChangeTheme(dark: mode.toShortString()); - } - // Synchronize the window theme of the system. - if (Platform.isMacOS) { - final isDark = mode == ThemeMode.dark; - RdPlatformChannel.instance.changeSystemWindowTheme( - isDark ? SystemWindowTheme.dark : SystemWindowTheme.light); + await bind.mainChangeTheme(dark: mode.toShortString()); + // Synchronize the window theme of the system. + updateSystemWindowTheme(); } } @@ -1694,12 +1690,12 @@ String getWindowNameWithId(String id, {WindowType? overrideType}) { return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; } -void updateSystemWindowTheme() { - // Set system window theme for macOS +Future updateSystemWindowTheme() async { + // Set system window theme for macOS. final userPreference = MyTheme.getThemeModePreference(); if (userPreference != ThemeMode.system) { if (Platform.isMacOS) { - RdPlatformChannel.instance.changeSystemWindowTheme( + await RdPlatformChannel.instance.changeSystemWindowTheme( userPreference == ThemeMode.light ? SystemWindowTheme.light : SystemWindowTheme.dark); diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart index 21f08f53f..1a36fb7a5 100644 --- a/flutter/lib/utils/platform_channel.dart +++ b/flutter/lib/utils/platform_channel.dart @@ -6,7 +6,7 @@ import 'package:flutter_hbb/main.dart'; enum SystemWindowTheme { light, dark } -/// The platform channel for RustDesk +/// The platform channel for RustDesk. class RdPlatformChannel { RdPlatformChannel._(); From 8dba3942052b322b2772fb929821940e8437f239 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 1 Feb 2023 20:58:21 +0800 Subject: [PATCH 294/734] scale system cursor image Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 1 - .../lib/desktop/widgets/remote_menubar.dart | 37 +++++++++---------- flutter/lib/models/model.dart | 34 ++++++++--------- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2e4668159..1687f348e 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_custom_cursor/cursor_manager.dart' diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 517dc9750..4f16f8227 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1133,26 +1133,23 @@ class _RemoteMenubarState extends State { }()); } - /// 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 { - state.value = v; - await bind.sessionToggleOption(id: widget.id, value: opt); - }, - padding: padding, - dismissOnClicked: true, - ); - }()); - } + 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 { + state.value = v; + await bind.sessionToggleOption(id: widget.id, value: opt); + }, + padding: padding, + dismissOnClicked: true, + ); + }()); /// Show quality monitor displayMenu.add(MenuEntrySwitch( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1eac1be39..78e6ce6af 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -778,28 +778,24 @@ class CursorData { scale = 1.0; } else { // Update data if scale changed. - if (Platform.isWindows) { - final tgtWidth = (width * scale).toInt(); - final tgtHeight = (width * scale).toInt(); - if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) { - double sw = kMinCursorSize.toDouble() / width; - double sh = kMinCursorSize.toDouble() / height; - scale = sw < sh ? sh : sw; - } + final tgtWidth = (width * scale).toInt(); + final tgtHeight = (width * scale).toInt(); + if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) { + double sw = kMinCursorSize.toDouble() / width; + double sh = kMinCursorSize.toDouble() / height; + scale = sw < sh ? sh : sw; } } - if (Platform.isWindows) { - if (_doubleToInt(oldScale) != _doubleToInt(scale)) { - data = img2 - .copyResize( - image!, - width: (width * scale).toInt(), - height: (height * scale).toInt(), - interpolation: img2.Interpolation.average, - ) - .getBytes(format: img2.Format.bgra); - } + if (_doubleToInt(oldScale) != _doubleToInt(scale)) { + data = img2 + .copyResize( + image!, + width: (width * scale).toInt(), + height: (height * scale).toInt(), + interpolation: img2.Interpolation.average, + ) + .getBytes(format: img2.Format.bgra); } this.scale = scale; From 8881462f748068a6118196212c9f98aebe4e3a31 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 1 Feb 2023 22:12:28 +0800 Subject: [PATCH 295/734] debug macos Signed-off-by: fufesou --- flutter/lib/models/model.dart | 37 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 78e6ce6af..b2df5faac 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -747,7 +747,7 @@ class CanvasModel with ChangeNotifier { class CursorData { final String peerId; final int id; - final img2.Image? image; + final img2.Image image; double scale; Uint8List? data; final double hotxOrigin; @@ -788,14 +788,27 @@ class CursorData { } if (_doubleToInt(oldScale) != _doubleToInt(scale)) { - data = img2 - .copyResize( - image!, - width: (width * scale).toInt(), - height: (height * scale).toInt(), - interpolation: img2.Interpolation.average, - ) - .getBytes(format: img2.Format.bgra); + if (Platform.isWindows) { + data = img2 + .copyResize( + image, + width: (width * scale).toInt(), + height: (height * scale).toInt(), + interpolation: img2.Interpolation.average, + ) + .getBytes(format: img2.Format.bgra); + } else { + data = Uint8List.fromList( + img2.encodePng( + img2.copyResize( + image, + width: (width * scale).toInt(), + height: (height * scale).toInt(), + interpolation: img2.Interpolation.average, + ), + ), + ); + } } this.scale = scale; @@ -863,7 +876,7 @@ class PredefinedCursor { _cache = CursorData( peerId: '', id: id, - image: _image2?.clone(), + image: _image2!.clone(), scale: scale, data: data, hotxOrigin: @@ -1063,9 +1076,9 @@ class CursorModel with ChangeNotifier { Future _updateCache( Uint8List rgba, ui.Image image, int id, int w, int h) async { Uint8List? data; - img2.Image? imgOrigin; + img2.Image imgOrigin = + img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba); if (Platform.isWindows) { - imgOrigin = img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba); data = imgOrigin.getBytes(format: img2.Format.bgra); } else { ByteData? imgBytes = From b5fbc23cb9b9fc7ba915e1024085d8ac7ad1bf18 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 12:39:39 +0800 Subject: [PATCH 296/734] zoom system cursor when view scale is adaptive Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 21 ++++++----- .../lib/desktop/widgets/remote_menubar.dart | 37 ++++++++++--------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 1687f348e..1ce9dec4a 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -362,10 +362,10 @@ class _RemotePageState extends State class ImagePaint extends StatefulWidget { final String id; - final Rx zoomCursor; - final Rx cursorOverImage; - final Rx keyboardEnabled; - final Rx remoteCursorMoved; + final RxBool zoomCursor; + final RxBool cursorOverImage; + final RxBool keyboardEnabled; + final RxBool remoteCursorMoved; final Widget Function(Widget)? listenerBuilder; ImagePaint( @@ -388,10 +388,10 @@ class _ImagePaintState extends State { final ScrollController _vertical = ScrollController(); String get id => widget.id; - Rx get zoomCursor => widget.zoomCursor; - Rx get cursorOverImage => widget.cursorOverImage; - Rx get keyboardEnabled => widget.keyboardEnabled; - Rx get remoteCursorMoved => widget.remoteCursorMoved; + RxBool get zoomCursor => widget.zoomCursor; + RxBool get cursorOverImage => widget.cursorOverImage; + RxBool get keyboardEnabled => widget.keyboardEnabled; + RxBool get remoteCursorMoved => widget.remoteCursorMoved; Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; @override @@ -466,7 +466,10 @@ class _ImagePaintState extends State { if (cache == null) { return MouseCursor.defer; } else { - final key = cache.updateGetKey(scale, zoomCursor.value); + final isViewAdaptive = + Provider.of(context, listen: false).viewStyle.style == + kRemoteViewStyleAdaptive; + final key = cache.updateGetKey(scale, zoomCursor.value && isViewAdaptive); if (!cursor.cachedKeys.contains(key)) { debugPrint("Register custom cursor with key $key"); // [Safety] diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4f16f8227..517dc9750 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1133,23 +1133,26 @@ class _RemoteMenubarState extends State { }()); } - 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 { - state.value = v; - await bind.sessionToggleOption(id: widget.id, value: opt); - }, - padding: padding, - dismissOnClicked: true, - ); - }()); + /// 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 { + state.value = v; + await bind.sessionToggleOption(id: widget.id, value: opt); + }, + padding: padding, + dismissOnClicked: true, + ); + }()); + } /// Show quality monitor displayMenu.add(MenuEntrySwitch( From 77ee60c8dc730af4b6658119b452cd1d417bba66 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 12:47:39 +0800 Subject: [PATCH 297/734] scale remote cursor when view style is adaptive Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 1ce9dec4a..858157853 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -635,7 +635,8 @@ class CursorPaint extends StatelessWidget { double x = (m.x - hotx) * c.scale + cx; double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; - if (zoomCursor.isTrue) { + final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; + if (zoomCursor.isTrue && isViewAdaptive) { x = m.x - hotx + cx / c.scale; y = m.y - hoty + cy / c.scale; scale = c.scale; From aafc2e0a8e4d0525b19ad011936354d6802bfb84 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 13:08:41 +0800 Subject: [PATCH 298/734] zoom cursor on different OSs Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 858157853..dd71797f3 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -466,10 +466,19 @@ class _ImagePaintState extends State { if (cache == null) { return MouseCursor.defer; } else { - final isViewAdaptive = - Provider.of(context, listen: false).viewStyle.style == - kRemoteViewStyleAdaptive; - final key = cache.updateGetKey(scale, zoomCursor.value && isViewAdaptive); + bool shouldScale = false; + if (Platform.isWindows) { + final isViewAdaptive = + Provider.of(context, listen: false).viewStyle.style == + kRemoteViewStyleAdaptive; + shouldScale = zoomCursor.value && isViewAdaptive; + } else { + final isViewOriginal = + Provider.of(context, listen: false).viewStyle.style == + kRemoteViewStyleOriginal; + shouldScale = zoomCursor.value || isViewOriginal; + } + final key = cache.updateGetKey(scale, shouldScale); if (!cursor.cachedKeys.contains(key)) { debugPrint("Register custom cursor with key $key"); // [Safety] @@ -635,8 +644,15 @@ class CursorPaint extends StatelessWidget { double x = (m.x - hotx) * c.scale + cx; double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; - final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; - if (zoomCursor.isTrue && isViewAdaptive) { + bool shouldScale = false; + if (Platform.isWindows) { + final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; + shouldScale = zoomCursor.value && isViewAdaptive; + } else { + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + shouldScale = zoomCursor.value || isViewOriginal; + } + if (shouldScale) { x = m.x - hotx + cx / c.scale; y = m.y - hoty + cy / c.scale; scale = c.scale; From d511d1e27a024847a50d31ec85392478907dccd6 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 13:40:13 +0800 Subject: [PATCH 299/734] zoom remote cursor when view style is original Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dd71797f3..f38cdfb6e 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -644,15 +644,8 @@ class CursorPaint extends StatelessWidget { double x = (m.x - hotx) * c.scale + cx; double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; - bool shouldScale = false; - if (Platform.isWindows) { - final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; - shouldScale = zoomCursor.value && isViewAdaptive; - } else { - final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; - shouldScale = zoomCursor.value || isViewOriginal; - } - if (shouldScale) { + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { x = m.x - hotx + cx / c.scale; y = m.y - hoty + cy / c.scale; scale = c.scale; From f9e3a3f074ffc3cfc43e398abb712d631497e930 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 14:39:58 +0800 Subject: [PATCH 300/734] zoom cursor with dpi Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 32 ++++++++++++---------- flutter/lib/models/model.dart | 15 +++++----- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f38cdfb6e..f7889d008 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -399,6 +399,20 @@ class _ImagePaintState extends State { final m = Provider.of(context); var c = Provider.of(context); final s = c.scale; + var cursorScale = 1.0; + + if (Platform.isWindows) { + // debug win10 + final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; + if (zoomCursor.value && isViewAdaptive) { + cursorScale = s * c.devicePixelRatio; + } + } else { + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { + cursorScale = s; + } + } mouseRegion({child}) => Obx(() => MouseRegion( cursor: cursorOverImage.isTrue @@ -414,10 +428,10 @@ class _ImagePaintState extends State { _lastRemoteCursorMoved = false; _firstEnterImage.value = true; } - return _buildCustomCursor(context, s); + return _buildCustomCursor(context, cursorScale); } }()) - : _buildDisabledCursor(context, s) + : _buildDisabledCursor(context, cursorScale) : MouseCursor.defer, onHover: (evt) {}, child: child)); @@ -466,19 +480,7 @@ class _ImagePaintState extends State { if (cache == null) { return MouseCursor.defer; } else { - bool shouldScale = false; - if (Platform.isWindows) { - final isViewAdaptive = - Provider.of(context, listen: false).viewStyle.style == - kRemoteViewStyleAdaptive; - shouldScale = zoomCursor.value && isViewAdaptive; - } else { - final isViewOriginal = - Provider.of(context, listen: false).viewStyle.style == - kRemoteViewStyleOriginal; - shouldScale = zoomCursor.value || isViewOriginal; - } - final key = cache.updateGetKey(scale, shouldScale); + final key = cache.updateGetKey(scale); if (!cursor.cachedKeys.contains(key)) { debugPrint("Register custom cursor with key $key"); // [Safety] diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b2df5faac..f49bb270c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -540,6 +540,7 @@ class CanvasModel with ChangeNotifier { double _y = 0; // image scale double _scale = 1.0; + double _devicePixelRatio = 1.0; Size _size = Size.zero; // the tabbar over the image // double tabBarHeight = 0.0; @@ -563,6 +564,7 @@ class CanvasModel with ChangeNotifier { double get x => _x; double get y => _y; double get scale => _scale; + double get devicePixelRatio => _devicePixelRatio; Size get size => _size; ScrollStyle get scrollStyle => _scrollStyle; ViewStyle get viewStyle => _lastViewStyle; @@ -611,8 +613,9 @@ class CanvasModel with ChangeNotifier { _lastViewStyle = viewStyle; _scale = viewStyle.scale; + _devicePixelRatio = ui.window.devicePixelRatio; if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { - _scale = 1.0 / ui.window.devicePixelRatio; + _scale = 1.0 / _devicePixelRatio; } _x = (size.width - displayWidth * _scale) / 2; _y = (size.height - displayHeight * _scale) / 2; @@ -772,11 +775,9 @@ class CursorData { int _doubleToInt(double v) => (v * 10e6).round().toInt(); - double _checkUpdateScale(double scale, bool shouldScale) { + double _checkUpdateScale(double scale) { double oldScale = this.scale; - if (!shouldScale) { - scale = 1.0; - } else { + if (scale != 1.0) { // Update data if scale changed. final tgtWidth = (width * scale).toInt(); final tgtHeight = (width * scale).toInt(); @@ -817,8 +818,8 @@ class CursorData { return scale; } - String updateGetKey(double scale, bool shouldScale) { - scale = _checkUpdateScale(scale, shouldScale); + String updateGetKey(double scale) { + scale = _checkUpdateScale(scale); return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; } } From 40a75e3dfab6707f3a87a36aee1a9f57460d8df1 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 2 Feb 2023 08:49:12 +0100 Subject: [PATCH 301/734] Update es.rs New 'Default' terms added. --- src/lang/es.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 6f866845c..5fdb7ee2c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -436,14 +436,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), + ("Display", "Pantalla"), + ("Default View Style", "Estilo de vista predeterminado"), + ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), + ("Default Image Quality", "Calidad de imagen predeterminada"), + ("Default Codec", "Códec predeterminado"), + ("Bitrate", "Tasa de bits"), ("FPS", ""), ("Auto", ""), - ("Other Default Options", ""), + ("Other Default Options", "Otras opciones predeterminadas"), ].iter().cloned().collect(); } From 1e9625045b222fd9d63013b37ceb88944ac5b6d9 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 2 Feb 2023 20:05:57 +0900 Subject: [PATCH 302/734] fix chat text selectable --- flutter/lib/common/widgets/chat_page.dart | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index d1d96199a..62f81b797 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -95,10 +95,31 @@ class ChatPage extends StatelessWidget implements PageShape { color: Theme.of(context).colorScheme.primary)), messageOptions: MessageOptions( showOtherUsersAvatar: false, - showTime: true, - currentUserTextColor: Colors.white, textColor: Colors.white, maxWidth: constraints.maxWidth * 0.7, + messageTextBuilder: (message, _, __) { + final isOwnMessage = + message.user.id == currentUser.id; + return Column( + crossAxisAlignment: isOwnMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Text(message.text, + style: TextStyle(color: Colors.white)), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Text( + "${message.createdAt.hour}:${message.createdAt.minute}", + style: TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ], + ); + }, messageDecorationBuilder: (_, __, ___) => defaultMessageDecoration( color: MyTheme.accent80, From c6269b54af37e60fb4a03e6f06623065ea998a0e Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 2 Feb 2023 21:39:25 +0900 Subject: [PATCH 303/734] add requestChatInputFocus() --- flutter/lib/models/chat_model.dart | 12 ++++++++++++ flutter/test/cm_test.dart | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 18a0be279..bab88a9dd 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; @@ -139,6 +141,7 @@ class ChatModel with ChangeNotifier { }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; + requestChatInputFocus(); } hideChatWindowOverlay() { @@ -188,6 +191,7 @@ class ChatModel with ChangeNotifier { await windowManager.setSizeAlignment( kConnectionManagerWindowSize, Alignment.topRight); } else { + requestChatInputFocus(); await windowManager.show(); await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight); _isShowCMChatPage = !_isShowCMChatPage; @@ -292,4 +296,12 @@ class ChatModel with ChangeNotifier { resetClientMode() { _messages[clientModeID]?.clear(); } + + void requestChatInputFocus() { + Timer(Duration(milliseconds: 100), () { + if (inputNode.hasListeners && inputNode.canRequestFocus) { + inputNode.requestFocus(); + } + }); + } } diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 592a28fcf..2c037c7b0 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -16,7 +16,7 @@ final testClients = [ Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) ]; -/// -t lib/cm_main.dart to test cm +/// flutter run -d {platform} -t lib/cm_test.dart to test cm void main(List args) async { isTest = true; WidgetsFlutterBinding.ensureInitialized(); From 0d9d506dac843986685c69ca72fdfd553d217f78 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Thu, 2 Feb 2023 14:48:22 +0100 Subject: [PATCH 304/734] Update it.rs --- src/lang/it.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 6edd4a461..d84b56a8a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -436,14 +436,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Visualizzazione"), + ("Default View Style", "Stile Visualizzazione Predefinito"), + ("Default Scroll Style", "Stile Scorrimento Predefinito"), + ("Default Image Quality", "Qualità Immagine Predefinita"), + ("Default Codec", "Codec Predefinito"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Altre Opzioni Predefinite"), ].iter().cloned().collect(); } From 1a1bd1b5d8c6be911451e288b577092795d967f4 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 2 Feb 2023 16:45:29 +0800 Subject: [PATCH 305/734] recover reordered peer tab, because flutter 3.7.0 fix ReorderableListView crash Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 235 +++++++++++++----- 1 file changed, 168 insertions(+), 67 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 278f5861c..fff5e2ffd 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; @@ -20,7 +21,9 @@ const int groupTabIndex = 4; const String defaultGroupTabname = 'Group'; class StatePeerTab { - final RxInt currentTab = 0.obs; + final RxInt currentTab = 0.obs; // index in tabNames + final RxList visibleOrderedTabs = RxList.empty(growable: true); + List tabOrder = List.from([0, 1, 2, 3, 4]); // constant length final RxInt tabHiddenFlag = 0.obs; final RxList tabNames = [ 'Recent Sessions', @@ -31,53 +34,80 @@ class StatePeerTab { ].obs; StatePeerTab._() { + // init tabHiddenFlag tabHiddenFlag.value = (int.tryParse( bind.getLocalFlutterConfig(k: 'hidden-peer-card'), radix: 2) ?? 0); var tabs = _notHiddenTabs(); + // remove dynamic tabs + tabs.remove(groupTabIndex); + // init tabOrder + try { + final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); + if (conf.isNotEmpty) { + final json = jsonDecode(conf); + if (json is List) { + final List list = + json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); + if (list.length == tabOrder.length && + tabOrder.every((e) => list.contains(e))) { + tabOrder = list; + } + } + } + } catch (e) { + debugPrintStack(label: '$e'); + } + // init visibleOrderedTabs + var tempList = tabOrder.toList(); + tempList.removeWhere((e) => !tabs.contains(e)); + visibleOrderedTabs.value = tempList; + // init currentTab currentTab.value = int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; if (!tabs.contains(currentTab.value)) { - currentTab.value = 0; + if (tabs.isNotEmpty) { + currentTab.value = tabs[0]; + } else { + currentTab.value = 0; + } } } static final StatePeerTab instance = StatePeerTab._(); + // check dynamic tabs check() { - var tabs = _notHiddenTabs(); - if (filterGroupCard()) { - if (currentTab.value == groupTabIndex) { - currentTab.value = - tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0; - bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: currentTab.value.toString()); - } + tabOrder2visibleOrderedTabs(); + if (visibleOrderedTabs.contains(groupTabIndex) && + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == + groupTabIndex) { + currentTab.value = groupTabIndex; + } + if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) { + tabNames[groupTabIndex] = gFFI.userModel.groupName.value; } else { - if (gFFI.userModel.isAdmin.isFalse && - gFFI.userModel.groupName.isNotEmpty) { - tabNames[groupTabIndex] = gFFI.userModel.groupName.value; - } else { - tabNames[groupTabIndex] = defaultGroupTabname; - } - if (tabs.contains(groupTabIndex) && - int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == - groupTabIndex) { - currentTab.value = groupTabIndex; - } + tabNames[groupTabIndex] = defaultGroupTabname; } } - List currentTabs() { - var v = List.empty(growable: true); - for (int i = 0; i < tabNames.length; i++) { - if (!_isTabHidden(i) && !_isTabFilter(i)) { - v.add(i); - } + visibleOrderedTabs2TabOrder() { + var tmpTabOrder = visibleOrderedTabs.toList(); + var left = tabOrder.where((e) => !tmpTabOrder.contains(e)).toList(); + for (var t in left) { + _addTabInOrder(tmpTabOrder, t); } - return v; + statePeerTab.tabOrder = tmpTabOrder; + bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder)); } + tabOrder2visibleOrderedTabs() { + var visible = statePeerTab.visibleTabs(); + statePeerTab.visibleOrderedTabs.value = + statePeerTab.tabOrder.where((e) => visible.contains(e)).toList(); + } + + // return true if hide group card bool filterGroupCard() { if (gFFI.groupModel.users.isEmpty || (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { @@ -87,6 +117,17 @@ class StatePeerTab { } } + // return index array of tabNames + List visibleTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i) && !_isTabFilter(i)) { + v.add(i); + } + } + return v; + } + bool _isTabHidden(int tabindex) { return tabHiddenFlag & (1 << tabindex) != 0; } @@ -107,6 +148,41 @@ class StatePeerTab { } return v; } + + // add tabIndex to list + _addTabInOrder(List list, int tabIndex) { + if (!tabOrder.contains(tabIndex) || list.contains(tabIndex)) { + return; + } + bool sameOrder = true; + int lastIndex = -1; + for (int i = 0; i < list.length; i++) { + var index = tabOrder.lastIndexOf(list[i]); + if (index > lastIndex) { + lastIndex = index; + continue; + } else { + sameOrder = false; + break; + } + } + if (sameOrder) { + var indexInTabOrder = tabOrder.indexOf(tabIndex); + var left = List.empty(growable: true); + for (int i = 0; i < indexInTabOrder; i++) { + left.add(tabOrder[i]); + } + int insertIndex = list.lastIndexWhere((e) => left.contains(e)); + if (insertIndex < 0) { + insertIndex = 0; + } else { + insertIndex += 1; + } + list.insert(insertIndex, tabIndex); + } else { + list.add(tabIndex); + } + } } final statePeerTab = StatePeerTab.instance; @@ -177,11 +253,6 @@ class _PeerTabPageState extends State } } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { return Column( @@ -215,40 +286,57 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; return Obx(() { - var tabs = statePeerTab.currentTabs(); - return ListView( + var tabs = statePeerTab.visibleOrderedTabs; + int indexCounter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + var list = tabs.toList(); + final int item = list.removeAt(oldIndex); + list.insert(newIndex, item); + tabs.value = list; + statePeerTab.visibleOrderedTabs2TabOrder(); + }, scrollDirection: Axis.horizontal, physics: NeverScrollableScrollPhysics(), - controller: ScrollController(), + scrollController: ScrollController(), children: tabs.map((t) { - return InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: statePeerTab.currentTab.value == t - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), - ), - child: Align( - alignment: Alignment.center, - child: Text( - translatedTabname(t), - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: statePeerTab.currentTab.value == t - ? textColor - : textColor - ?..withOpacity(0.5)), + indexCounter++; + return ReorderableDragStartListener( + key: ValueKey(t), + index: indexCounter, + child: InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: statePeerTab.currentTab.value == t + ? Theme.of(context).backgroundColor + : null, + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), ), - )), - onTap: () async { - await handleTabSelection(t); - await bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: t.toString()); - }, + child: Align( + alignment: Alignment.center, + child: Text( + translatedTabname(t), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: statePeerTab.currentTab.value == t + ? textColor + : textColor + ?..withOpacity(0.5)), + ), + )), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, + ), ); }).toList()); }); @@ -275,7 +363,7 @@ class _PeerTabPageState extends State final verticalMargin = isDesktop ? 12.0 : 6.0; return Expanded( child: Obx(() { - var tabs = statePeerTab.currentTabs(); + var tabs = statePeerTab.visibleOrderedTabs; if (tabs.isEmpty) { return visibleContextMenuListener(Center( child: Text(translate('Right click to select tabs')), @@ -322,7 +410,7 @@ class _PeerTabPageState extends State } adjustTab() { - var tabs = statePeerTab.currentTabs(); + var tabs = statePeerTab.visibleOrderedTabs; if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) { statePeerTab.currentTab.value = tabs[0]; } @@ -349,11 +437,13 @@ class _PeerTabPageState extends State Widget visibleContextMenu(CancelFunc cancelFunc) { return Obx(() { final List menu = List.empty(growable: true); + final List menuIndex = List.empty(growable: true); for (int i = 0; i < statePeerTab.tabNames.length; i++) { if (i == groupTabIndex && statePeerTab.filterGroupCard()) { continue; } int bitMask = 1 << i; + menuIndex.add(i); menu.add(MenuEntrySwitch( switchType: SwitchType.scheckbox, text: translatedTabname(i), @@ -369,12 +459,21 @@ class _PeerTabPageState extends State await bind.setLocalFlutterConfig( k: 'hidden-peer-card', v: statePeerTab.tabHiddenFlag.value.toRadixString(2)); + statePeerTab.tabOrder2visibleOrderedTabs(); cancelFunc(); adjustTab(); })); } + // show in tabOrder + List menu2 = List.empty(growable: true); + statePeerTab.tabOrder.map((e) { + final index = menuIndex.indexOf(e); + if (index >= 0) { + menu2.add(menu[index]); + } + }).toList(); return mod_menu.PopupMenu( - items: menu + items: menu2 .map((entry) => entry.build( context, const MenuConfig( @@ -421,7 +520,9 @@ class _PeerSearchBarState extends State { FocusNode focusNode = FocusNode(); focusNode.addListener(() { focused.value = focusNode.hasFocus; - peerSearchTextController.selection = TextSelection(baseOffset: 0, extentOffset: peerSearchTextController.value.text.length); + peerSearchTextController.selection = TextSelection( + baseOffset: 0, + extentOffset: peerSearchTextController.value.text.length); }); return Container( width: 120, From 50c8855d2816897527fb65c4cd01c8e1f7f16c6a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 3 Feb 2023 16:18:08 +0800 Subject: [PATCH 306/734] unify peer tab text color with tab bar text color --- flutter/lib/common/widgets/peer_tab_page.dart | 5 ++--- flutter/pubspec.lock | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index fff5e2ffd..150121c59 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -284,7 +284,6 @@ class _PeerTabPageState extends State } Widget _createSwitchBar(BuildContext context) { - final textColor = Theme.of(context).textTheme.titleLarge?.color; return Obx(() { var tabs = statePeerTab.visibleOrderedTabs; int indexCounter = -1; @@ -326,8 +325,8 @@ class _PeerTabPageState extends State height: 1, fontSize: 14, color: statePeerTab.currentTab.value == t - ? textColor - : textColor + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor ?..withOpacity(0.5)), ), )), diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index c193c0651..ebb105178 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -475,10 +475,10 @@ packages: dependency: "direct main" description: name: flutter_custom_cursor - sha256: "6c5204cf6a16650355b8aa47a8402e79922c07641390a32021a1069b561909ec" + sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d" url: "https://pub.dev" source: hosted - version: "0.0.3" + version: "0.0.4" flutter_improved_scrolling: dependency: "direct main" description: From e05b95743c2f67bfaee077f4338007c9c6e16238 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 17:02:28 +0800 Subject: [PATCH 307/734] cursor position and size update Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 77 +++++++++++-------- .../widgets/material_mod_popup_menu.dart | 16 +++- .../lib/desktop/widgets/remote_menubar.dart | 22 ++++-- flutter/lib/models/input_model.dart | 2 - flutter/lib/models/model.dart | 2 +- 5 files changed, 74 insertions(+), 45 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f7889d008..0e0127312 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -399,42 +399,51 @@ class _ImagePaintState extends State { final m = Provider.of(context); var c = Provider.of(context); final s = c.scale; - var cursorScale = 1.0; - if (Platform.isWindows) { - // debug win10 - final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; - if (zoomCursor.value && isViewAdaptive) { - cursorScale = s * c.devicePixelRatio; - } - } else { - final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; - if (zoomCursor.value || isViewOriginal) { - cursorScale = s; - } - } + mouseRegion({child}) => Obx(() { + double getCursorScale() { + var c = Provider.of(context); + var cursorScale = 1.0; + if (Platform.isWindows) { + // debug win10 + final isViewAdaptive = + c.viewStyle.style == kRemoteViewStyleAdaptive; + if (zoomCursor.value && isViewAdaptive) { + cursorScale = s * c.devicePixelRatio; + } + } else { + final isViewOriginal = + c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { + cursorScale = s; + } + } + return cursorScale; + } - mouseRegion({child}) => Obx(() => MouseRegion( - cursor: cursorOverImage.isTrue - ? c.cursorEmbedded - ? SystemMouseCursors.none - : keyboardEnabled.isTrue - ? (() { - if (remoteCursorMoved.isTrue) { - _lastRemoteCursorMoved = true; - return SystemMouseCursors.none; - } else { - if (_lastRemoteCursorMoved) { - _lastRemoteCursorMoved = false; - _firstEnterImage.value = true; - } - return _buildCustomCursor(context, cursorScale); - } - }()) - : _buildDisabledCursor(context, cursorScale) - : MouseCursor.defer, - onHover: (evt) {}, - child: child)); + return MouseRegion( + cursor: cursorOverImage.isTrue + ? c.cursorEmbedded + ? SystemMouseCursors.none + : keyboardEnabled.isTrue + ? (() { + if (remoteCursorMoved.isTrue) { + _lastRemoteCursorMoved = true; + return SystemMouseCursors.none; + } else { + if (_lastRemoteCursorMoved) { + _lastRemoteCursorMoved = false; + _firstEnterImage.value = true; + } + return _buildCustomCursor( + context, getCursorScale()); + } + }()) + : _buildDisabledCursor(context, getCursorScale()) + : MouseCursor.defer, + onHover: (evt) {}, + child: child); + }); if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { final imageWidth = c.getDisplayWidth() * s; diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index a371e8f52..666c9a6e2 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -790,6 +790,7 @@ class _PopupMenuRoute extends PopupRoute { _PopupMenuRoute({ required this.position, required this.items, + this.menuWrapper, this.initialValue, this.elevation, required this.barrierLabel, @@ -802,6 +803,7 @@ class _PopupMenuRoute extends PopupRoute { final RelativeRect position; final List> items; + final MenuWrapper? menuWrapper; final List itemSizes; final T? initialValue; final double? elevation; @@ -844,11 +846,14 @@ class _PopupMenuRoute extends PopupRoute { } } - final Widget menu = _PopupMenu( + Widget menu = _PopupMenu( route: this, semanticLabel: semanticLabel, constraints: constraints, ); + if (this.menuWrapper != null) { + menu = this.menuWrapper!(menu); + } final MediaQueryData mediaQuery = MediaQuery.of(context); return MediaQuery.removePadding( context: context, @@ -1035,6 +1040,7 @@ Future showMenu({ required BuildContext context, required RelativeRect position, required List> items, + MenuWrapper? menuWrapper, T? initialValue, double? elevation, String? semanticLabel, @@ -1062,6 +1068,7 @@ Future showMenu({ return navigator.push(_PopupMenuRoute( position: position, items: items, + menuWrapper: menuWrapper, initialValue: initialValue, elevation: elevation, semanticLabel: semanticLabel, @@ -1094,6 +1101,8 @@ typedef PopupMenuCanceled = void Function(); typedef PopupMenuItemBuilder = List> Function( BuildContext context); +typedef MenuWrapper = Widget Function(Widget child); + /// Displays a menu when pressed and calls [onSelected] when the menu is dismissed /// because an item was selected. The value passed to [onSelected] is the value of /// the selected menu item. @@ -1124,6 +1133,7 @@ class PopupMenuButton extends StatefulWidget { const PopupMenuButton({ Key? key, required this.itemBuilder, + this.menuWrapper, this.initialValue, this.onHover, this.onSelected, @@ -1151,6 +1161,9 @@ class PopupMenuButton extends StatefulWidget { /// Called when the button is pressed to create the items to show in the menu. final PopupMenuItemBuilder itemBuilder; + /// Menu wrapper. + final MenuWrapper? menuWrapper; + /// The value of the menu item, if any, that should be highlighted when the menu opens. final T? initialValue; @@ -1333,6 +1346,7 @@ class PopupMenuButtonState extends State> { context: context, elevation: widget.elevation ?? popupMenuTheme.elevation, items: items, + menuWrapper: widget.menuWrapper, initialValue: widget.initialValue, position: position, shape: widget.shape ?? popupMenuTheme.shape, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 64d289fcc..db1721d99 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -221,6 +221,16 @@ class _RemoteMenubarState extends State { } } + Widget _buildPointerTrackWidget(Widget child) { + return Listener( + onPointerHover: (PointerHoverEvent e) => + widget.ffi.inputModel.lastMousePos = e.position, + child: MouseRegion( + child: child, + ), + ); + } + Widget _buildMenubar(BuildContext context) { final List menubarItems = []; if (!isWebDesktop) { @@ -379,13 +389,10 @@ class _RemoteMenubarState extends State { mod_menu.PopupMenuItem( height: _MenubarTheme.height, padding: EdgeInsets.zero, - child: Listener( - onPointerHover: (PointerHoverEvent e) => - widget.ffi.inputModel.lastMousePos = e.position, - child: MouseRegion( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: rowChildren), + child: _buildPointerTrackWidget( + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: rowChildren, ), ), ) @@ -435,6 +442,7 @@ class _RemoteMenubarState extends State { ), tooltip: translate('Display Settings'), position: mod_menu.PopupMenuPosition.under, + menuWrapper: _buildPointerTrackWidget, itemBuilder: (BuildContext context) => _getDisplayMenu(snapshot.data!, remoteCount) .map((entry) => entry.build( diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index d2f671cdc..8c37f50bd 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -310,7 +310,6 @@ class InputModel { } } - int _signOrZero(num x) { if (x == 0) { return 0; @@ -362,7 +361,6 @@ class InputModel { trackpadScrollDistance = Offset.zero; } - void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); if (e.kind != ui.PointerDeviceKind.mouse) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f49bb270c..da711bf13 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -244,7 +244,6 @@ class FfiModel with ChangeNotifier { parent.target?.canvasModel.updateViewStyle(); } parent.target?.recordingModel.onSwitchDisplay(); - parent.target?.inputModel.refreshMousePos(); notifyListeners(); } @@ -621,6 +620,7 @@ class CanvasModel with ChangeNotifier { _y = (size.height - displayHeight * _scale) / 2; _imageOverflow.value = _x < 0 || y < 0; notifyListeners(); + parent.target?.inputModel.refreshMousePos(); } updateScrollStyle() async { From 17aac13247a94bbf35a8a169a4b50f8a4b14a9f4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 18:28:47 +0800 Subject: [PATCH 308/734] ignore first update cursor postion Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 3 ++- flutter/lib/models/model.dart | 7 ++++++- src/client.rs | 8 ++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index db1721d99..2a84dcf14 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1157,8 +1157,9 @@ class _RemoteMenubarState extends State { return state; }, setter: (bool v) async { - state.value = v; await bind.sessionToggleOption(id: widget.id, value: opt); + state.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: opt); }, padding: padding, dismissOnClicked: true, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index da711bf13..8a7a1005d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -904,6 +904,7 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; + bool _firstUpdateMousePos = false; bool gotMouseControl = true; DateTime _lastPeerMouse = DateTime.now() .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec)); @@ -1121,7 +1122,11 @@ class CursorModel with ChangeNotifier { /// Update the cursor position. updateCursorPosition(Map evt, String id) async { - gotMouseControl = false; + if (!_firstUpdateMousePos) { + _firstUpdateMousePos = true; + } else { + gotMouseControl = false; + } _lastPeerMouse = DateTime.now(); _x = double.parse(evt['x']); _y = double.parse(evt['y']); diff --git a/src/client.rs b/src/client.rs index fb42ce840..e0ac68c5d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1117,8 +1117,12 @@ impl LoginConfigHandler { } else if name == "show-quality-monitor" { config.show_quality_monitor.v = !config.show_quality_monitor.v; } else { - let v = self.options.get(&name).is_some(); - if v { + let is_set = self + .options + .get(&name) + .map(|o| !o.is_empty()) + .unwrap_or(false); + if is_set { self.config.options.remove(&name); } else { self.config.options.insert(name, "Y".to_owned()); From 66851efaa3ca2b9c5274ed80b7e43c155d4ff789 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 3 Feb 2023 17:08:40 +0800 Subject: [PATCH 309/734] fix: --connect command on macOS & window closing issues --- flutter/lib/common.dart | 22 ++++++++++-------- flutter/lib/consts.dart | 3 ++- .../lib/desktop/widgets/tabbar_widget.dart | 12 ++++++---- flutter/lib/main.dart | 8 +++---- flutter/lib/utils/multi_window_manager.dart | 23 ++++++++++++++++++- flutter/lib/utils/platform_channel.dart | 6 +++++ flutter/macos/Runner/AppDelegate.swift | 9 ++++---- flutter/macos/Runner/Info.plist | 10 ++++---- flutter/macos/Runner/MainFlutterWindow.swift | 3 +++ 9 files changed, 68 insertions(+), 28 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a2623ff15..c058ec434 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3,14 +3,11 @@ import 'dart:convert'; import 'dart:ffi' hide Size; import 'dart:io'; import 'dart:math'; -import 'dart:typed_data'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_hbb/utils/platform_channel.dart'; -import 'package:win32/win32.dart' as win32; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -19,14 +16,17 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/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:uni_links/uni_links.dart'; import 'package:uni_links_desktop/uni_links_desktop.dart'; -import 'package:window_manager/window_manager.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:window_size/window_size.dart' as window_size; import 'package:url_launcher/url_launcher.dart'; +import 'package:win32/win32.dart' as win32; +import 'package:window_manager/window_manager.dart'; +import 'package:window_size/window_size.dart' as window_size; +import '../consts.dart'; import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/remote_page.dart'; @@ -34,8 +34,6 @@ import 'models/input_model.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; -import '../consts.dart'; - final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); @@ -1275,9 +1273,11 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { /// initUniLinks should only be used on macos/windows. /// we use dbus for linux currently. Future initUniLinks() async { - if (!Platform.isWindows && !Platform.isMacOS) { + if (Platform.isLinux) { return; } + // Register uni links for Windows. The required info of url scheme is already + // declared in `Info.plist` for macOS. if (Platform.isWindows) { registerProtocol('rustdesk'); } @@ -1508,8 +1508,12 @@ Future onActiveWindowChanged() async { } catch (err) { debugPrintStack(label: "$err"); } finally { + debugPrint("Start closing RustDesk..."); await windowManager.setPreventClose(false); await windowManager.close(); + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } } } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index e4081d9a5..f48b612a8 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; const double kDesktopRemoteTabBarHeight = 28.0; +const int kMainWindowId = 0; const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 598b2cc4c..223076951 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,23 +1,23 @@ -import 'dart:io'; import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; -import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:bot_toast/bot_toast.dart'; import '../../utils/multi_window_manager.dart'; @@ -527,7 +527,9 @@ class WindowActionPanelState extends State void onWindowClose() async { // hide window on close if (widget.isMainWindow) { - await rustDeskWinManager.unregisterActiveWindow(0); + 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. diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 5b1e0c37c..b41cc17df 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,22 +1,22 @@ import 'dart:convert'; import 'dart:io'; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; -import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart'; +import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:bot_toast/bot_toast.dart'; // import 'package:window_manager/window_manager.dart'; @@ -139,8 +139,8 @@ void runMainApp(bool startService) async { rustDeskWinManager.registerActiveWindow(kWindowMainId); } windowManager.setOpacity(1); + windowManager.setTitle(getWindowName()); }); - windowManager.setTitle(getWindowName()); } void runMobileApp() async { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 550e9ab08..3af189ef6 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -160,6 +160,24 @@ class RustDeskMultiWindowManager { return null; } + void clearWindowType(WindowType type) { + switch (type) { + case WindowType.Main: + return; + case WindowType.RemoteDesktop: + _remoteDesktopWindowId = null; + break; + case WindowType.FileTransfer: + _fileTransferWindowId = null; + break; + case WindowType.PortForward: + _portForwardWindowId = null; + break; + case WindowType.Unknown: + break; + } + } + void setMethodHandler( Future Function(MethodCall call, int fromWindowId)? handler) { DesktopMultiWindow.setMethodHandler(handler); @@ -186,8 +204,11 @@ class RustDeskMultiWindowManager { } await WindowController.fromWindowId(wId).setPreventClose(false); await WindowController.fromWindowId(wId).close(); - } on Error { + } catch (e) { + debugPrint("$e"); return; + } finally { + clearWindowType(type); } } } diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart index 1a36fb7a5..7b60ef63c 100644 --- a/flutter/lib/utils/platform_channel.dart +++ b/flutter/lib/utils/platform_channel.dart @@ -31,4 +31,10 @@ class RdPlatformChannel { return _osxMethodChannel .invokeMethod("setWindowTheme", {"themeName": theme.name}); } + + /// Terminate .app manually. + Future terminate() { + assert(Platform.isMacOS); + return _osxMethodChannel.invokeMethod("terminate"); + } } diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 5708e35cb..3498decd3 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -3,21 +3,22 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { - var lauched = false; + var launched = false; override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { dummy_method_to_enforce_bundling() - return true + // https://github.com/leanflutter/window_manager/issues/214 + return false } override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { - if (lauched) { + if (launched) { handle_applicationShouldOpenUntitledFile(); } return true } override func applicationDidFinishLaunching(_ aNotification: Notification) { - lauched = true; + launched = true; NSApplication.shared.activate(ignoringOtherApps: true); } } diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index d1077e0e4..c926019ab 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -23,8 +23,10 @@ CFBundleTypeRole Editor - CFBundleURLName + CFBundleURLIconFile + CFBundleURLName + com.carriez.rustdesk CFBundleURLSchemes rustdesk @@ -35,13 +37,13 @@ $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) + LSUIElement + 1 NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass - NSApplication - LSUIElement - 1 + NSApplication diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index cea1e94bb..042840569 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -78,6 +78,9 @@ class MainFlutterWindow: NSWindow { self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light") result(nil) break; + case "terminate": + NSApplication.shared.terminate(self) + result(nil) default: result(FlutterMethodNotImplemented) } From c13c89c0d6f09a14daea21b4a2e4cf5dd4bd4dff Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 3 Feb 2023 18:52:22 +0800 Subject: [PATCH 310/734] fix: uni links cause main window show --- flutter/lib/common.dart | 17 ++++++++++------- flutter/lib/main.dart | 7 +++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c058ec434..7e22e0848 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1272,9 +1272,9 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { /// [Availability] /// initUniLinks should only be used on macos/windows. /// we use dbus for linux currently. -Future initUniLinks() async { +Future initUniLinks() async { if (Platform.isLinux) { - return; + return false; } // Register uni links for Windows. The required info of url scheme is already // declared in `Info.plist` for macOS. @@ -1285,11 +1285,12 @@ Future initUniLinks() async { try { final initialLink = await getInitialLink(); if (initialLink == null) { - return; + return false; } - parseRustdeskUri(initialLink); + return parseRustdeskUri(initialLink); } catch (err) { debugPrintStack(label: "$err"); + return false; } } @@ -1310,11 +1311,13 @@ StreamSubscription? listenUniLinks() { return sub; } -/// Returns true if we successfully handle the startup arguments. +/// Handle command line arguments +/// +/// * Returns true if we successfully handle the startup arguments. bool checkArguments() { // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args - final connectIndex = kBootArgs.indexOf("--connect"); + var connectIndex = kBootArgs.indexOf("--connect"); if (connectIndex == -1) { return false; } @@ -1368,7 +1371,7 @@ bool callUniLinksUriHandler(Uri uri) { Future.delayed(Duration.zero, () { rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid); }); - return false; + return true; } return false; } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b41cc17df..67a243eff 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -114,7 +114,6 @@ Future initEnv(String appType) async { void runMainApp(bool startService) async { // register uni links - initUniLinks(); await initEnv(kAppTypeMain); // trigger connection status updater await bind.mainCheckConnectStatus(); @@ -130,7 +129,11 @@ void runMainApp(bool startService) async { // Restore the location of the main window before window hide or show. await restoreWindowPosition(WindowType.Main); // Check the startup argument, if we successfully handle the argument, we keep the main window hidden. - if (checkArguments()) { + final handledByUniLinks = await initUniLinks(); + final handledByCli = checkArguments(); + debugPrint( + "handled by uni links: $handledByUniLinks, handled by cli: $handledByCli"); + if (handledByUniLinks || handledByCli) { windowManager.hide(); } else { windowManager.show(); From ca97826b80e09979f29f2c96993e6125d42e0e36 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 19:17:59 +0800 Subject: [PATCH 311/734] update cursor position when menu is dismissed Signed-off-by: fufesou --- flutter/lib/desktop/widgets/popup_menu.dart | 55 ++++++++++++++++--- .../lib/desktop/widgets/remote_menubar.dart | 45 ++++++++++++++- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 0cbdad929..9833dcbca 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -109,13 +109,17 @@ class MenuConfig { this.boxWidth}); } +typedef DismissCallback = Function(); + abstract class MenuEntryBase { bool dismissOnClicked; + DismissCallback? dismissCallback; RxBool? enabled; MenuEntryBase({ this.dismissOnClicked = false, this.enabled, + this.dismissCallback, }); List> build(BuildContext context, MenuConfig conf); @@ -146,12 +150,14 @@ class MenuEntryRadioOption { String value; bool dismissOnClicked; RxBool? enabled; + DismissCallback? dismissCallback; MenuEntryRadioOption({ required this.text, required this.value, this.dismissOnClicked = false, this.enabled, + this.dismissCallback, }); } @@ -177,8 +183,13 @@ class MenuEntryRadios extends MenuEntryBase { required this.optionSetter, this.padding, dismissOnClicked = false, + dismissCallback, RxBool? enabled, - }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) { + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + dismissCallback: dismissCallback, + ) { () async { _curOption.value = await curOptionGetter(); }(); @@ -249,6 +260,9 @@ class MenuEntryRadios extends MenuEntryBase { onPressed() { if (opt.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (opt.dismissCallback != null) { + opt.dismissCallback!(); + } } setOption(opt.value); } @@ -360,6 +374,9 @@ class MenuEntrySubRadios extends MenuEntryBase { onPressed: () { if (opt.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (opt.dismissCallback != null) { + opt.dismissCallback!(); + } } setOption(opt.value); }, @@ -421,7 +438,12 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { this.textStyle, this.padding, RxBool? enabled, - }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled); + dismissCallback, + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + dismissCallback: dismissCallback, + ); RxBool get curOption; Future setOption(bool? option); @@ -463,6 +485,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(v); }, @@ -474,6 +499,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(v); }, @@ -485,6 +513,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { onPressed: () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(!curOption.value); }, @@ -508,6 +539,7 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( switchType: switchType, text: text, @@ -515,6 +547,7 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { padding: padding, dismissOnClicked: dismissOnClicked, enabled: enabled, + dismissCallback: dismissCallback, ) { () async { _curOption.value = await getter(); @@ -551,12 +584,15 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( - switchType: switchType, - text: text, - textStyle: textStyle, - padding: padding, - dismissOnClicked: dismissOnClicked); + switchType: switchType, + text: text, + textStyle: textStyle, + padding: padding, + dismissOnClicked: dismissOnClicked, + dismissCallback: dismissCallback, + ); @override RxBool get curOption => getter(); @@ -627,9 +663,11 @@ class MenuEntryButton extends MenuEntryBase { this.padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( dismissOnClicked: dismissOnClicked, enabled: enabled, + dismissCallback: dismissCallback, ); Widget _buildChild(BuildContext context, MenuConfig conf) { @@ -641,6 +679,9 @@ class MenuEntryButton extends MenuEntryBase { ? () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } proc(); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2a84dcf14..5b418bcc4 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -231,6 +231,8 @@ class _RemoteMenubarState extends State { ); } + _menuDismissCallback() => widget.ffi.inputModel.refreshMousePos(); + Widget _buildMenubar(BuildContext context) { final List menubarItems = []; if (!isWebDesktop) { @@ -374,6 +376,7 @@ class _RemoteMenubarState extends State { onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); + _menuDismissCallback(); } RxInt display = CurrentDisplayState.find(widget.id); if (display.value != i) { @@ -551,6 +554,7 @@ class _RemoteMenubarState extends State { onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); + _menuDismissCallback(); } showSetOSPassword( widget.id, false, widget.ffi.dialogManager); @@ -563,6 +567,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -574,6 +579,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -585,6 +591,7 @@ class _RemoteMenubarState extends State { connect(context, widget.id, isTcpTunneling: true); }, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ]); // {handler.get_audit_server() &&

  • {translate('Note')}
  • } @@ -602,6 +609,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ); } @@ -618,6 +626,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } } @@ -635,6 +644,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } @@ -649,6 +659,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); if (pi.platform == kPeerPlatformWindows) { @@ -667,6 +678,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } if (pi.platform != kPeerPlatformAndroid && @@ -681,6 +693,7 @@ class _RemoteMenubarState extends State { showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager), padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } } @@ -696,6 +709,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } @@ -717,6 +731,7 @@ class _RemoteMenubarState extends State { // }, // padding: padding, // dismissOnClicked: true, + // dismissCallback: _menuDismissCallback, // )); // } } @@ -762,11 +777,13 @@ class _RemoteMenubarState extends State { text: translate('Scale original'), value: kRemoteViewStyleOriginal, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( text: translate('Scale adaptive'), value: kRemoteViewStyleAdaptive, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ], curOptionGetter: () async { @@ -782,6 +799,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryDivider(), MenuEntryRadios( @@ -791,21 +809,26 @@ class _RemoteMenubarState extends State { text: translate('Good image quality'), value: kRemoteImageQualityBest, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( text: translate('Balanced'), value: kRemoteImageQualityBalanced, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( text: translate('Optimize reaction time'), value: kRemoteImageQualityLow, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( - text: translate('Custom'), - value: kRemoteImageQualityCustom, - dismissOnClicked: true), + 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 @@ -970,12 +993,14 @@ class _RemoteMenubarState extends State { 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, ), ], @@ -988,6 +1013,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); displayMenu.insert(3, MenuEntryDivider()); @@ -1058,6 +1084,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ); } @@ -1084,11 +1111,13 @@ class _RemoteMenubarState extends State { text: translate('Auto'), value: 'auto', dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( text: 'VP9', value: 'vp9', dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ]; if (codecs[0]) { @@ -1096,6 +1125,7 @@ class _RemoteMenubarState extends State { text: 'H264', value: 'h264', dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } if (codecs[1]) { @@ -1103,6 +1133,7 @@ class _RemoteMenubarState extends State { text: 'H265', value: 'h265', dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } return list; @@ -1119,6 +1150,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } } @@ -1141,6 +1173,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ); }()); } @@ -1163,6 +1196,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ); }()); } @@ -1182,6 +1216,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); final perms = widget.ffi.ffiModel.permissions; @@ -1219,6 +1254,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } } @@ -1290,6 +1326,7 @@ class _RemoteMenubarState extends State { onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); + _menuDismissCallback(); } showKBLayoutTypeChooser( localPlatform, widget.ffi.dialogManager); @@ -1302,6 +1339,7 @@ class _RemoteMenubarState extends State { proc: () {}, padding: EdgeInsets.zero, dismissOnClicked: false, + dismissCallback: _menuDismissCallback, ), ); } @@ -1321,6 +1359,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: dismissOnClicked, + dismissCallback: _menuDismissCallback, ); } } From 0940c93a481b90652af890e320ace5c4e00dde3e Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 20:27:05 +0800 Subject: [PATCH 312/734] show cursor on conn is established Signed-off-by: fufesou --- flutter/lib/models/model.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8a7a1005d..d032719e9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -904,10 +904,10 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; - bool _firstUpdateMousePos = false; + DateTime? _firstUpdateMouseTime; bool gotMouseControl = true; DateTime _lastPeerMouse = DateTime.now() - .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec)); + .subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec)); String id = ''; WeakReference parent; @@ -926,6 +926,15 @@ class CursorModel with ChangeNotifier { DateTime.now().difference(_lastPeerMouse).inMilliseconds < kMouseControlTimeoutMSec; + bool isConnIn2Secs() { + if (_firstUpdateMouseTime == null) { + _firstUpdateMouseTime = DateTime.now(); + return true; + } else { + return DateTime.now().difference(_firstUpdateMouseTime!).inSeconds < 2; + } + } + CursorModel(this.parent); Set get cachedKeys => _cacheKeys; @@ -1122,12 +1131,10 @@ class CursorModel with ChangeNotifier { /// Update the cursor position. updateCursorPosition(Map evt, String id) async { - if (!_firstUpdateMousePos) { - _firstUpdateMousePos = true; - } else { + if (!isConnIn2Secs()) { gotMouseControl = false; + _lastPeerMouse = DateTime.now(); } - _lastPeerMouse = DateTime.now(); _x = double.parse(evt['x']); _y = double.parse(evt['y']); try { From 0d36166ea88847510426433dee115e17c693fe25 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 23:07:47 +0800 Subject: [PATCH 313/734] sync option after toggle Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_tab_page.dart | 6 +++--- flutter/lib/desktop/widgets/remote_menubar.dart | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 55124fbcc..d832db0c6 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -273,6 +273,7 @@ class _ConnectionTabPageState extends State { menu.add(MenuEntryDivider()); menu.add(() { final state = ShowRemoteCursorState.find(key); + final optKey = 'show-remote-cursor'; return MenuEntrySwitch2( switchType: SwitchType.scheckbox, text: translate('Show remote cursor'), @@ -280,9 +281,8 @@ class _ConnectionTabPageState extends State { return state; }, setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: key, value: 'show-remote-cursor'); + await bind.sessionToggleOption(id: key, value: optKey); + state.value = bind.sessionGetToggleOptionSync(id: key, arg: optKey); cancelFunc(); }, padding: padding, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 5b418bcc4..d6b1cec72 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1160,6 +1160,7 @@ class _RemoteMenubarState extends State { if (!widget.ffi.canvasModel.cursorEmbedded) { displayMenu.add(() { final state = ShowRemoteCursorState.find(widget.id); + final optKey = 'show-remote-cursor'; return MenuEntrySwitch2( switchType: SwitchType.scheckbox, text: translate('Show remote cursor'), @@ -1167,9 +1168,9 @@ class _RemoteMenubarState extends State { return state; }, setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); + await bind.sessionToggleOption(id: widget.id, value: optKey); + state.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: optKey); }, padding: padding, dismissOnClicked: true, From 96a7182ff85ce35f47d46a4c5ff8c9a3258bad15 Mon Sep 17 00:00:00 2001 From: solokot Date: Fri, 3 Feb 2023 20:05:48 +0300 Subject: [PATCH 314/734] update ru.rs --- src/lang/ru.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 7ec6c1554..54b064c18 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -436,14 +436,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "Переключить стороны"), ("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", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Отображение"), + ("Default View Style", "Стиль отображения по умолчанию"), + ("Default Scroll Style", "Стиль прокрутки по умолчанию"), + ("Default Image Quality", "Качество изображения по умолчанию"), + ("Default Codec", "Кодек по умолчанию"), + ("Bitrate", "Битрейт"), + ("FPS", "FPS"), + ("Auto", "Авто"), + ("Other Default Options", "Другие параметры по умолчанию"), ].iter().cloned().collect(); } From f9d106ea745017b1c7c2e8c69b2ea1ba6dc670c4 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 3 Feb 2023 22:36:50 +0100 Subject: [PATCH 315/734] Update de.rs --- src/lang/de.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 5b68c0e7a..2d6d3d069 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -200,7 +200,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Warnung"), ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt."), ("Reboot required", "Neustart erforderlich"), - ("Unsupported display server ", "Nicht unterstützter Display-Server"), + ("Unsupported display server ", "Nicht unterstützter Anzeigeserver"), ("x11 expected", "X11 erwartet"), ("Port", "Port"), ("Settings", "Einstellungen"), @@ -327,7 +327,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mobile Actions", "Mobile Aktionen"), ("Select Monitor", "Bildschirm auswählen"), ("Control Actions", "Aktionen"), - ("Display Settings", "Bildschirmeinstellungen"), + ("Display Settings", "Anzeigeeinstellungen"), ("Ratio", "Verhältnis"), ("Image Quality", "Bildqualität"), ("Scroll Style", "Scroll-Stil"), @@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), ("Scale original", "Keine Skalierung"), - ("Scale adaptive", "Automatische Skalierung"), + ("Scale adaptive", "Anpassbare Skalierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), ("Theme", "Farbgebung"), @@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clear", "Zurücksetzen"), ("Audio Input Device", "Audioeingabegerät"), ("Deny remote access", "Fernzugriff verbieten"), - ("Use IP Whitelisting", "IP-Whitelist benutzen"), + ("Use IP Whitelisting", "IP-Whitelist verwenden"), ("Network", "Netzwerk"), ("Enable RDP", "RDP aktivieren"), ("Pin menubar", "Menüleiste anpinnen"), @@ -436,14 +436,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Anzeige"), + ("Default View Style", "Standard-Ansichtsstil"), + ("Default Scroll Style", "Standard-Scroll-Stil"), + ("Default Image Quality", "Standard-Bildqualität"), + ("Default Codec", "Standard-Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "fps"), + ("Auto", "Automatisch"), + ("Other Default Options", "Weitere Standardoptionen"), ].iter().cloned().collect(); } From 3a1b9781124e0a59f64c5ea4cbc30ebdf1fe742d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 01:10:32 +0800 Subject: [PATCH 316/734] feat: add event handler on rust macos --- Cargo.lock | 41 +++++++++++++++++-- Cargo.toml | 2 + .../macos/Runner.xcodeproj/project.pbxproj | 5 ++- flutter/macos/Runner/MainFlutterWindow.swift | 1 - src/ui/macos.rs | 27 ++++++++++-- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c4af56e9..e15641363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,7 +1137,7 @@ checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a" dependencies = [ "dconf_rs", "detect-desktop-environment", - "dirs", + "dirs 4.0.0", "objc", "rust-ini", "web-sys", @@ -1401,6 +1401,16 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1873,6 +1883,19 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fruitbasket" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898289b8e0528c84fb9b88f15ac9d5109bcaf23e0e49bb6f64deee0d86b6a351" +dependencies = [ + "dirs 2.0.2", + "objc", + "objc-foundation", + "objc_id", + "time 0.1.45", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -3657,6 +3680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -3670,6 +3694,15 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -4655,6 +4688,7 @@ dependencies = [ "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", + "fruitbasket", "glib 0.16.5", "gtk", "hbb_common", @@ -4673,6 +4707,7 @@ dependencies = [ "mouce", "num_cpus", "objc", + "objc_id", "parity-tokio-ipc", "rdev", "repng", @@ -4713,7 +4748,7 @@ name = "rustdesk-portable-packer" version = "0.1.0" dependencies = [ "brotli", - "dirs", + "dirs 4.0.0", "embed-resource", "md5", ] @@ -6591,7 +6626,7 @@ dependencies = [ "async-trait", "byteorder", "derivative", - "dirs", + "dirs 4.0.0", "enumflags2", "event-listener", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 1e9af30e5..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,8 @@ core-graphics = "0.22" include_dir = "0.7.2" tray-item = "0.7" # looks better than trayicon dark-light = "0.2" +fruitbasket = "0.10.0" +objc_id = "0.1.1" [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.25" } diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 7a17c3de1..066560203 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; + LastSwiftMigration = 1420; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { @@ -463,6 +463,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Profile; @@ -607,6 +608,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -643,6 +645,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_VERSION = 5.0; }; diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 042840569..97b46bb84 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -87,4 +87,3 @@ class MainFlutterWindow: NSWindow { }) } } - diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 7daef8eab..39812cf90 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -1,3 +1,5 @@ +use std::{ffi::c_void, rc::Rc}; + #[cfg(target_os = "macos")] use cocoa::{ appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*, NSMenu, NSMenuItem}, @@ -8,11 +10,14 @@ use objc::{ class, declare::ClassDecl, msg_send, - runtime::{Object, Sel, BOOL}, + runtime::{BOOL, Object, Sel}, sel, sel_impl, }; -use sciter::{make_args, Host}; -use std::{ffi::c_void, rc::Rc}; +use objc::runtime::Class; +use objc_id::WeakId; +use sciter::{Host, make_args}; + +use hbb_common::log; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -98,12 +103,21 @@ 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)); let decl = decl.register(); let delegate: id = msg_send![decl, alloc]; let () = msg_send![delegate, init]; let state = DelegateState { handler }; let handler_ptr = Box::into_raw(Box::new(state)); (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); + // Set the url scheme handler + let cls = Class::get("NSAppleEventManager").unwrap(); + let manager: *mut Object = msg_send![cls, sharedAppleEventManager]; + let _: () = msg_send![manager, + setEventHandler: delegate + andSelector: sel!(handleEvent:withReplyEvent:) + forEventClass: fruitbasket::kInternetEventClass + andEventID: fruitbasket::kAEGetURL]; let () = msg_send![NSApp(), setDelegate: delegate]; } @@ -167,6 +181,13 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } +extern 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!("event found {}", url); + let _ = crate::run_me(vec![url]); +} + unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { let title = NSString::alloc(nil).init_str(title); let action = sel!(handleMenuItem:); From 7e69cbde26a1a62f3e319fdfebd12637a2dd2956 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 01:22:40 +0800 Subject: [PATCH 317/734] opt: support binary + uri links startup --- flutter/lib/common.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 7e22e0848..9f3e2c740 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1315,6 +1315,12 @@ StreamSubscription? listenUniLinks() { /// /// * Returns true if we successfully handle the startup arguments. bool checkArguments() { + if (kBootArgs.isNotEmpty) { + final ret = parseRustdeskUri(kBootArgs.first); + if (ret) { + return true; + } + } // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args var connectIndex = kBootArgs.indexOf("--connect"); @@ -1352,7 +1358,7 @@ bool checkArguments() { bool parseRustdeskUri(String uriPath) { final uri = Uri.tryParse(uriPath); if (uri == null) { - print("uri is not valid: $uriPath"); + debugPrint("uri is not valid: $uriPath"); return false; } return callUniLinksUriHandler(uri); From a9fc63c34f6f0bb18701e9597d1a5d8568c35ccc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 01:31:56 +0800 Subject: [PATCH 318/734] opt: add default url scheme handler for macos --- src/flutter_ffi.rs | 31 ++++++++++++++++++------------- src/ui/macos.rs | 9 +++++++-- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d40c66d19..d001dd388 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -3,28 +3,29 @@ use std::{ ffi::{CStr, CString}, os::raw::c_char, }; +use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; -use crate::common::is_keyboard_mode_supported; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; use hbb_common::{ - config::{self, LocalConfig, PeerConfig, ONLINE}, + config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, }; -use std::str::FromStr; +use hbb_common::message_proto::KeyboardMode; +use hbb_common::ResultType; -// use crate::hbbs_http::account::AuthResult; - -use crate::flutter::{self, SESSIONS}; -use crate::ui_interface::{self, *}; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, flutter::{session_add, session_start_}, }; +use crate::common::is_keyboard_mode_supported; +use crate::flutter::{self, SESSIONS}; +use crate::ui_interface::{self, *}; + +// use crate::hbbs_http::account::AuthResult; + fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); #[cfg(target_os = "android")] @@ -910,6 +911,11 @@ pub fn main_start_dbus_server() { } } +pub fn osx_handle_uni_links(url: String) { + #![cfg(target_os = "macos")] + crate::ui::macos::handle_url_scheme(url); +} + pub fn session_send_mouse(id: String, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { let alt = m.get("alt").is_some(); @@ -1257,13 +1263,12 @@ pub fn main_hide_docker() -> SyncReturn { #[cfg(target_os = "android")] pub mod server_side { + use hbb_common::log; use jni::{ + JNIEnv, objects::{JClass, JString}, sys::jstring, - JNIEnv, - }; - - use hbb_common::log; + }; use crate::start_server; diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 39812cf90..94e75959c 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -181,11 +181,16 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } -extern fn handle_apple_event(this: &Object, _cmd: Sel, event: u64, _reply: u64) { +/// The function to handle the url scheme sent by system. +pub fn handle_url_scheme(url: String) { + unimplemented!(); +} + +extern 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!("event found {}", url); - let _ = crate::run_me(vec![url]); + handle_url_scheme(url); } unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { From 4dfae8da1075450346ae72927faac8fc659027d5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 11:23:36 +0800 Subject: [PATCH 319/734] feat: add url scheme handler for macos --- flutter/lib/consts.dart | 3 +- flutter/lib/main.dart | 2 +- flutter/lib/models/model.dart | 3 ++ flutter/lib/models/native_model.dart | 10 +++- src/flutter_ffi.rs | 25 +++++++--- src/ipc.rs | 11 +++- src/server.rs | 75 ++++++++++++++++++++++------ src/ui/macos.rs | 18 +++++-- 8 files changed, 116 insertions(+), 31 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index f48b612a8..1fc97f410 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -11,8 +11,9 @@ const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformAndroid = "Android"; -/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page" const String kAppTypeMain = "main"; +const String kAppTypeConnectionManager = "cm"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopPortForward = "port forward"; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 67a243eff..86cc9d89b 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -211,7 +211,7 @@ void runMultiWindow( } void runConnectionManagerScreen(bool hide) async { - await initEnv(kAppTypeMain); + await initEnv(kAppTypeConnectionManager); _runApp( '', const DesktopServerPage(), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d032719e9..aae4c6a07 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -199,6 +199,9 @@ class FfiModel with ChangeNotifier { final peer_id = evt['peer_id'].toString(); await bind.sessionSwitchSides(id: peer_id); closeConnection(id: peer_id); + } else if (name == "on_url_scheme_received") { + final url = evt['url'].toString(); + parseRustdeskUri(url); } }; } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index cf2de4219..d6885bfb0 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -8,6 +8,7 @@ import 'package:external_path/external_path.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart' as win32; @@ -46,6 +47,8 @@ class PlatformFFI { static get localeName => Platform.localeName; + static get isMain => instance._appType == kAppTypeMain; + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -112,8 +115,11 @@ class PlatformFFI { } _ffiBind = RustdeskImpl(dylib); if (Platform.isLinux) { - // start dbus service, no need to await - await _ffiBind.mainStartDbusServer(); + // Start a dbus service, no need to await + _ffiBind.mainStartDbusServer(); + } else if (Platform.isMacOS) { + // Start an ipc server for handling url schemes. + _ffiBind.mainStartIpcUrlServer(); } _startListenEvent(_ffiBind); // global event try { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d001dd388..5dccd9050 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,8 +1,4 @@ -use std::{ - collections::HashMap, - ffi::{CStr, CString}, - os::raw::c_char, -}; +use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char, thread}; use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; @@ -1261,6 +1257,23 @@ pub fn main_hide_docker() -> SyncReturn { SyncReturn(true) } +/// Start an ipc server for receiving the url scheme. +/// +/// * Should only be called in the main flutter window. +/// * macOS only +pub fn main_start_ipc_url_server() { + #[cfg(target_os = "macos")] + thread::spawn(move || crate::server::start_ipc_url_server()); +} + +/// Send a url scheme throught the ipc. +/// +/// * macOS only +pub fn send_url_scheme(url: String) { + #[cfg(target_os = "macos")] + thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::log; @@ -1268,7 +1281,7 @@ pub mod server_side { JNIEnv, objects::{JClass, JString}, sys::jstring, - }; + }; use crate::start_server; diff --git a/src/ipc.rs b/src/ipc.rs index d4d803aec..d610fb84d 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -16,10 +16,10 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, timeout, tokio, + log, password_security as password, ResultType, timeout, + tokio, tokio::io::{AsyncRead, AsyncWrite}, tokio_util::codec::Framed, - ResultType, }; use crate::rendezvous_mediator::RendezvousMediator; @@ -210,6 +210,7 @@ pub enum Data { DataPortableService(DataPortableService), SwitchSidesRequest(String), SwitchSidesBack, + UrlLink(String) } #[tokio::main(flavor = "current_thread")] @@ -832,3 +833,9 @@ pub async fn test_rendezvous_server() -> ResultType<()> { c.send(&Data::TestRendezvousServer).await?; Ok(()) } + +#[tokio::main(flavor = "current_thread")] +pub async fn send_url_scheme(url: String) -> ResultType<()> { + connect(1_000, "_url").await?.send(&Data::UrlLink(url)).await?; + Ok(()) +} diff --git a/src/server.rs b/src/server.rs index 381e3df90..de213ae5a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,13 @@ -use crate::ipc::Data; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; + use bytes::Bytes; + pub use connection::*; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::config::Config2; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -12,19 +17,19 @@ use hbb_common::{ message_proto::*, protobuf::{Enum, Message as _}, rendezvous_proto::*, + ResultType, socket_client, - sodiumoxide::crypto::{box_, secretbox, sign}, - timeout, tokio, ResultType, Stream, + sodiumoxide::crypto::{box_, secretbox, sign}, Stream, timeout, tokio, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use service::ServiceTmpl; +use hbb_common::config::Config2; +use hbb_common::tcp::new_listener; use service::{GenericService, Service, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use service::ServiceTmpl; + +use crate::ipc::{connect, Data}; +use crate::ui_interface::SENDER; pub mod audio_service; cfg_if::cfg_if! { @@ -55,8 +60,6 @@ mod service; mod video_qos; pub mod video_service; -use hbb_common::tcp::new_listener; - pub type Childs = Arc>>; type ConnMap = HashMap; @@ -425,6 +428,50 @@ pub async fn start_server(is_server: bool) { } } +#[cfg(target_os = "macos")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc_url_server() { + log::debug!("Start an ipc server for listening to url schemes"); + match crate::ipc::new_listener("_url").await { + Ok(mut incoming) => { + while let Some(Ok(conn)) = incoming.next().await { + let mut conn = crate::ipc::Connection::new(conn); + match conn.next_timeout(1000).await { + Ok(Some(data)) => { + match data { + Data::UrlLink(url) => { + #[cfg(feature = "flutter")] + { + if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM.read().unwrap().get( + crate::flutter::APP_TYPE_MAIN + ) { + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + stream.add(serde_json::to_string(&m).unwrap()); + } else { + log::warn!("No main window app found!"); + } + } + } + _ => { + log::warn!("An unexpected data was sent to the ipc url server.") + } + } + } + Err(err) => { + log::error!("{}", err); + } + _ => {} + } + } + } + Err(err) => { + log::error!("{}", err); + } + } +} + #[cfg(target_os = "macos")] async fn sync_and_watch_config_dir() { if crate::platform::is_root() { diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 94e75959c..98e355dc1 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -17,7 +17,9 @@ use objc::runtime::Class; use objc_id::WeakId; use sciter::{Host, make_args}; -use hbb_common::log; +use hbb_common::{log, tokio}; + +use crate::ui_cm_interface::start_ipc; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -181,16 +183,22 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } -/// The function to handle the url scheme sent by system. +/// 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) { - unimplemented!(); + 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 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!("event found {}", url); - handle_url_scheme(url); + log::debug!("an event was received: {}", url); + std::thread::spawn(move || handle_url_scheme(url)); } unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { From a349be6428cca3675d777821b71554e4e49b0952 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 11:33:08 +0800 Subject: [PATCH 320/734] opt: remove unnecessary ffi func --- src/flutter_ffi.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5dccd9050..ca9314c43 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -907,11 +907,6 @@ pub fn main_start_dbus_server() { } } -pub fn osx_handle_uni_links(url: String) { - #![cfg(target_os = "macos")] - crate::ui::macos::handle_url_scheme(url); -} - pub fn session_send_mouse(id: String, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { let alt = m.get("alt").is_some(); From 151b115fc900ad15fd2fc79319e166b48c9b6661 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 13:37:48 +0800 Subject: [PATCH 321/734] fix: android build --- src/server.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server.rs b/src/server.rs index de213ae5a..109fc1e9a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,7 +29,6 @@ use service::{GenericService, Service, Subscriber}; use service::ServiceTmpl; use crate::ipc::{connect, Data}; -use crate::ui_interface::SENDER; pub mod audio_service; cfg_if::cfg_if! { From dd00ea5abd24be98addc5444294f52908cbc729f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 16:18:54 +0800 Subject: [PATCH 322/734] opt: reuse current main window when using url scheme --- src/core_main.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 89a962f1d..99d0e888e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,6 @@ -use hbb_common::log; +use std::future::Future; + +use hbb_common::{log, ResultType}; /// shared by flutter and sciter main function /// @@ -346,5 +348,11 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option Date: Sun, 5 Feb 2023 07:59:29 +0330 Subject: [PATCH 323/734] Update fa.rs --- src/lang/fa.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 72cde49f9..dd1c75bac 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -436,14 +436,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "طرفین را عوض کنید"), ("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", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "نمایش دادن"), + ("Default View Style", "سبک نمایش پیش فرض"), + ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), + ("Default Image Quality", "کیفیت تصویر پیش فرض"), + ("Default Codec", "کدک پیش فرض"), + ("Bitrate", "میزان بیت صفحه نمایش"), + ("FPS", "FPS"), + ("Auto", "خودکار"), + ("Other Default Options", "سایر گزینه های پیش فرض"), ].iter().cloned().collect(); } From afb76c63261ac5d2f1602ec3d7627a1168ee11c6 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Sun, 5 Feb 2023 10:20:05 +0330 Subject: [PATCH 324/734] Update README-FA.md --- docs/README-FA.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README-FA.md b/docs/README-FA.md index 02b156dbb..496e81849 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -1,6 +1,6 @@

    RustDesk - Your remote desktop
    -
    تصاویر محیط نرم‌افزار • + تصاویر محیط نرم‌افزارساختارداکرساخت • @@ -9,12 +9,12 @@

    [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]

    برای ترجمه این سند (README)، رابط کاربری RustDesk، و مستندات آن به زبان مادری شما به کمکتان نیازمندیم.

    -با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) +با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -راست‌دسک (RustDesk) نرم‌افزاری برای گارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. +راست‌دسک (RustDesk) نرم‌افزاری برای کارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا [ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). @@ -130,7 +130,7 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -سپس، هر بار که نیاز به ساخت ترم‌افزار داشتید، دستور زیر را اجرا کنید: +سپس، هر بار که نیاز به ساخت نرم‌افزار داشتید، دستور زیر را اجرا کنید: ```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 From 3462756a11a8b69bed40246cd4a6362b291f7bfd Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 5 Feb 2023 16:56:13 +0800 Subject: [PATCH 325/734] optimize dialog margin, fix password eye icon color --- flutter/lib/common.dart | 8 +++----- flutter/lib/consts.dart | 1 - flutter/lib/mobile/widgets/dialog.dart | 5 +---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9f3e2c740..8236597ff 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -694,7 +694,6 @@ void msgBox(String id, String type, String title, String text, String link, buttons.insert( 0, dialogButton('Cancel', onPressed: cancel, isOutline: true)); } - // TODO: test this button if (type.contains("hasclose")) { buttons.insert( 0, @@ -708,8 +707,7 @@ void msgBox(String id, String type, String title, String text, String link, dialogManager.show( (setState, close) => CustomAlertDialog( title: null, - content: SelectionArea( - child: msgboxContent(type, title, text).paddingOnly(bottom: 10)), + content: SelectionArea(child: msgboxContent(type, title, text)), actions: buttons, onSubmit: hasOk ? submit : null, onCancel: hasCancel == true ? cancel : null, @@ -774,7 +772,7 @@ Widget msgboxContent(String type, String title, String text) { ), ), ], - ); + ).marginOnly(bottom: 12); } void msgBoxCommon(OverlayDialogManager dialogManager, String title, @@ -1714,4 +1712,4 @@ Future updateSystemWindowTheme() async { : SystemWindowTheme.dark); } } -} \ No newline at end of file +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 1fc97f410..c95c62fcc 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -26,7 +26,6 @@ const String kWindowEventShow = "show"; const String kWindowConnect = "connect"; const String kUniLinksPrefix = "rustdesk://"; -const String kActionNewConnection = "connection/new/"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index bded6d069..2fbe40091 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/desktop/widgets/button.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -371,8 +370,7 @@ void showWaitUacDialog( tag: '$id-wait-uac', (setState, close) => CustomAlertDialog( title: null, - content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip') - .marginOnly(bottom: 10), + content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'), )); } @@ -647,7 +645,6 @@ class _PasswordWidgetState extends State { icon: Icon( // Based on passwordVisible state choose the icon _passwordVisible ? Icons.visibility : Icons.visibility_off, - color: Theme.of(context).primaryColorDark, ), onPressed: () { // Update the state i.e. toggle the state of passwordVisible variable From 255c58ef7b725ea64012073ae8d8cb48720d7b98 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 5 Feb 2023 17:29:54 +0800 Subject: [PATCH 326/734] opt: close button color and corner on tab --- flutter/lib/desktop/widgets/tabbar_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 223076951..cfbddbafb 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -959,7 +959,7 @@ class _CloseButton extends StatelessWidget { offstage: !visible, child: InkWell( hoverColor: MyTheme.tabbar(context).closeHoverColor, - customBorder: const RoundedRectangleBorder(), + customBorder: const CircleBorder(), onTap: () => onClose(), child: Icon( Icons.close, @@ -1082,7 +1082,7 @@ class TabbarTheme extends ThemeExtension { unSelectedIconColor: Color.fromARGB(255, 96, 96, 96), dividerColor: Color.fromARGB(255, 238, 238, 238), hoverColor: Color.fromARGB(51, 158, 158, 158), - closeHoverColor: Colors.black, + closeHoverColor: Color.fromARGB(255, 224, 224, 224), selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240)); static const dark = TabbarTheme( From 133fba573bea02d9a29b64e879d522f13d331069 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 5 Feb 2023 18:20:22 +0800 Subject: [PATCH 327/734] confirmed issue #2935 is false report, set_bitrate was called, and bandwidth has obvious change if you watch car game video --- src/server/video_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index d041a433c..55920e320 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -498,7 +498,7 @@ fn run(sp: GenericService) -> ResultType<()> { video_qos.target_bitrate, video_qos.fps ); - encoder.set_bitrate(video_qos.target_bitrate).unwrap(); + allow_err!(encoder.set_bitrate(video_qos.target_bitrate)); spf = video_qos.spf(); } drop(video_qos); From bb0f481df31fc6121a2c5782c158d0d4a3d3f66c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 5 Feb 2023 17:00:22 -0700 Subject: [PATCH 328/734] update rust build action to use the same on all --- .github/workflows/flutter-nightly.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 83ad7629e..5ca284cee 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -59,10 +59,9 @@ jobs: - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: - toolchain: "1.62" + toolchain: stable target: ${{ matrix.job.target }} override: true - components: rustfmt profile: minimal # minimal component installation (ie, no documentation) - uses: Swatinem/rust-cache@v2 From c306ec3ba76217f24d66384f31c1d8fecb388291 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 6 Feb 2023 09:54:21 +0900 Subject: [PATCH 329/734] opt chat window on its overlay, make window focusable as a desktop app --- flutter/lib/common/widgets/overlay.dart | 64 ++++++++++++++----------- flutter/lib/models/chat_model.dart | 31 ++++++++++-- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 4b4172ffd..d84789d9c 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,6 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import '../../consts.dart'; @@ -91,28 +92,31 @@ class DraggableChatWindow extends StatelessWidget { bottom: BorderSide( color: Theme.of(context).hintColor.withOpacity(0.4)))), height: 38, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - child: Row(children: [ - Icon(Icons.chat_bubble_outline, - size: 20, color: Theme.of(context).colorScheme.primary), - SizedBox(width: 6), - Text(translate("Chat")) - ])), - Padding( - padding: EdgeInsets.all(2), - child: ActionIcon( - message: 'Close', - icon: IconFont.close, - onTap: chatModel.hideChatWindowOverlay, - isClose: true, - boxSize: 32, - )) - ], - ), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row(children: [ + Icon(Icons.chat_bubble_outline, + size: 20, color: Theme.of(context).colorScheme.primary), + SizedBox(width: 6), + Text(translate("Chat")) + ])), + Padding( + padding: EdgeInsets.all(2), + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ))), ); } } @@ -304,15 +308,17 @@ class _DraggableState extends State { if (widget.checkKeyboard) { checkKeyboard(); } - if (widget.checkKeyboard) { + if (widget.checkScreenSize) { checkScreenSize(); } - return Positioned( - top: _position.dy, - left: _position.dx, - width: widget.width, - height: widget.height, - child: widget.builder(context, onPanUpdate)); + return Stack(children: [ + Positioned( + top: _position.dy, + left: _position.dx, + width: widget.width, + height: widget.height, + child: widget.builder(context, onPanUpdate)) + ]); } } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index bab88a9dd..dd35bd22f 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -4,6 +4,8 @@ import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -37,6 +39,8 @@ class ChatModel with ChangeNotifier { OverlayEntry? chatWindowOverlayEntry; bool isConnManager = false; + RxBool isWindowFocus = true.obs; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -133,11 +137,28 @@ class ChatModel with ChangeNotifier { final overlayState = _getOverlayState(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - return DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this); + bool innerClicked = false; + return Listener( + onPointerDown: (_) { + if (!innerClicked) { + isWindowFocus.value = false; + } + innerClicked = false; + }, + child: Obx(() => Container( + color: isWindowFocus.value ? Colors.red.withOpacity(0.3) : null, + child: Listener( + onPointerDown: (_) { + innerClicked = true; + if (!isWindowFocus.value) { + isWindowFocus.value = true; + } + }, + child: DraggableChatWindow( + position: const Offset(20, 80), + width: 250, + height: 350, + chatModel: this))))); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; From 74dc2b253832e9cd6cab2f5c9e08d19d8a479b4b Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 6 Feb 2023 11:27:20 +0800 Subject: [PATCH 330/734] refactor remote menu Signed-off-by: fufesou --- .../lib/desktop/pages/remote_tab_page.dart | 93 +----- .../lib/desktop/widgets/remote_menubar.dart | 278 ++++++++++++------ 2 files changed, 207 insertions(+), 164 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index d832db0c6..9b00b481f 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -243,96 +243,35 @@ class _ConnectionTabPageState extends State { padding: padding, ), MenuEntryDivider(), - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Scale original'), - value: kRemoteViewStyleOriginal, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Scale adaptive'), - value: kRemoteViewStyleAdaptive, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetViewStyle(id: key) ?? '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetViewStyle(id: key, value: newValue); - ffi.canvasModel.updateViewStyle(); - cancelFunc(); - }, - padding: padding, + RemoteMenuEntry.viewStyle( + key, + ffi, + padding, + dismissFunc: cancelFunc, ), ]); if (!ffi.canvasModel.cursorEmbedded) { menu.add(MenuEntryDivider()); - menu.add(() { - final state = ShowRemoteCursorState.find(key); - final optKey = 'show-remote-cursor'; - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: key, value: optKey); - state.value = bind.sessionGetToggleOptionSync(id: key, arg: optKey); - cancelFunc(); - }, - padding: padding, - ); - }()); + menu.add(RemoteMenuEntry.showRemoteCursor( + key, + padding, + dismissFunc: cancelFunc, + )); } if (perms['keyboard'] != false) { if (perms['clipboard'] != false) { - menu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate('Disable clipboard'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: key, arg: 'disable-clipboard'); - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: key, value: 'disable-clipboard'); - cancelFunc(); - }, - padding: padding, - )); + menu.add(RemoteMenuEntry.disableClipboard(key, padding, + dismissFunc: cancelFunc)); } - menu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Insert Lock'), - style: style, - ), - proc: () { - bind.sessionLockScreen(id: key); - cancelFunc(); - }, - padding: padding, - dismissOnClicked: true, - )); + menu.add( + RemoteMenuEntry.insertLock(key, padding, dismissFunc: cancelFunc)); if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { - menu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', - style: style, - ), - proc: () { - bind.sessionCtrlAltDel(id: key); - cancelFunc(); - }, - padding: padding, - dismissOnClicked: true, - )); + menu.add(RemoteMenuEntry.insertCtrlAltDel(key, padding, + dismissFunc: cancelFunc)); } } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d6b1cec72..36b9504c0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -99,6 +99,175 @@ class _MenubarTheme { static const double dividerHeight = 12.0; } +typedef DismissFunc = void Function(); + +class RemoteMenuEntry { + static MenuEntryRadios viewStyle( + String remoteId, + FFI ffi, + EdgeInsets padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + RxString? rxViewStyle, + }) { + return MenuEntryRadios( + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Scale original'), + value: kRemoteViewStyleOriginal, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ), + MenuEntryRadioOption( + text: translate('Scale adaptive'), + value: kRemoteViewStyleAdaptive, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ), + ], + curOptionGetter: () async { + // null means peer id is not found, which there's no need to care about + final viewStyle = await bind.sessionGetViewStyle(id: remoteId) ?? ''; + if (rxViewStyle != null) { + rxViewStyle.value = viewStyle; + } + return viewStyle; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionSetViewStyle(id: remoteId, value: newValue); + if (rxViewStyle != null) { + rxViewStyle.value = newValue; + } + ffi.canvasModel.updateViewStyle(); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch2 showRemoteCursor( + String remoteId, + EdgeInsets padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + final state = ShowRemoteCursorState.find(remoteId); + final optKey = 'show-remote-cursor'; + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: remoteId, value: optKey); + state.value = + bind.sessionGetToggleOptionSync(id: remoteId, arg: optKey); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch disableClipboard( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return createSwitchMenuEntry( + remoteId, + 'Disable clipboard', + 'disable-clipboard', + padding, + true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch createSwitchMenuEntry( + String remoteId, + String text, + String option, + EdgeInsets? padding, + bool dismissOnClicked, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate(text), + getter: () async { + return bind.sessionGetToggleOptionSync(id: remoteId, arg: option); + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: remoteId, value: option); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: dismissOnClicked, + dismissCallback: dismissCallback, + ); + } + + static MenuEntryButton insertLock( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Insert Lock'), + style: style, + ), + proc: () { + bind.sessionLockScreen(id: remoteId); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static insertCtrlAltDel( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + '${translate("Insert")} Ctrl + Alt + Del', + style: style, + ), + proc: () { + bind.sessionCtrlAltDel(id: remoteId); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } +} + class RemoteMenubar extends StatefulWidget { final String id; final FFI ffi; @@ -616,18 +785,8 @@ class _RemoteMenubarState extends State { displayMenu.add(MenuEntryDivider()); if (perms['keyboard'] != false) { if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', - style: style, - ), - proc: () { - bind.sessionCtrlAltDel(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); + displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding, + dismissCallback: _menuDismissCallback)); } } if (perms['restart'] != false && @@ -649,18 +808,8 @@ class _RemoteMenubarState extends State { } if (perms['keyboard'] != false) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Insert Lock'), - style: style, - ), - proc: () { - bind.sessionLockScreen(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); + displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding, + dismissCallback: _menuDismissCallback)); if (pi.platform == kPeerPlatformWindows) { displayMenu.add(MenuEntryButton( @@ -770,36 +919,12 @@ class _RemoteMenubarState extends State { const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); final peer_version = widget.ffi.ffiModel.pi.version; final displayMenu = [ - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Scale original'), - value: kRemoteViewStyleOriginal, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryRadioOption( - text: translate('Scale adaptive'), - value: kRemoteViewStyleAdaptive, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ], - curOptionGetter: () async { - // null means peer id is not found, which there's no need to care about - final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; - widget.state.viewStyle.value = viewStyle; - return viewStyle; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetViewStyle(id: widget.id, value: newValue); - widget.state.viewStyle.value = newValue; - widget.ffi.canvasModel.updateViewStyle(); - }, - padding: padding, - dismissOnClicked: true, + RemoteMenuEntry.viewStyle( + widget.id, + widget.ffi, + padding, dismissCallback: _menuDismissCallback, + rxViewStyle: widget.state.viewStyle, ), MenuEntryDivider(), MenuEntryRadios( @@ -1158,25 +1283,11 @@ class _RemoteMenubarState extends State { /// Show remote cursor if (!widget.ffi.canvasModel.cursorEmbedded) { - displayMenu.add(() { - final state = ShowRemoteCursorState.find(widget.id); - final optKey = 'show-remote-cursor'; - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: optKey); - state.value = - bind.sessionGetToggleOptionSync(id: widget.id, arg: optKey); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ); - }()); + displayMenu.add(RemoteMenuEntry.showRemoteCursor( + widget.id, + padding, + dismissCallback: _menuDismissCallback, + )); } /// Show remote cursor scaling with image @@ -1237,8 +1348,11 @@ class _RemoteMenubarState extends State { if (perms['keyboard'] != false) { if (perms['clipboard'] != false) { - displayMenu.add(_createSwitchMenuEntry( - 'Disable clipboard', 'disable-clipboard', padding, true)); + displayMenu.add(RemoteMenuEntry.disableClipboard( + widget.id, + padding, + dismissCallback: _menuDismissCallback, + )); } displayMenu.add(_createSwitchMenuEntry( 'Lock after session end', 'lock-after-session-end', padding, true)); @@ -1349,19 +1463,9 @@ class _RemoteMenubarState extends State { MenuEntrySwitch _createSwitchMenuEntry( String text, String option, EdgeInsets? padding, bool dismissOnClicked) { - return MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate(text), - getter: () async { - return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: option); - }, - padding: padding, - dismissOnClicked: dismissOnClicked, - dismissCallback: _menuDismissCallback, - ); + return RemoteMenuEntry.createSwitchMenuEntry( + widget.id, text, option, padding, dismissOnClicked, + dismissCallback: _menuDismissCallback); } } From 40d0ea016bae6dca3b51f503f25003312f129299 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 3 Feb 2023 15:07:45 +0800 Subject: [PATCH 331/734] refactor peer tab with model, make it scrollable Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 449 ++++++------------ flutter/lib/main.dart | 1 + flutter/lib/mobile/widgets/dialog.dart | 6 +- flutter/lib/models/group_model.dart | 4 +- flutter/lib/models/model.dart | 5 +- flutter/lib/models/peer_tab_model.dart | 275 +++++++++++ flutter/lib/models/user_model.dart | 2 +- 7 files changed, 423 insertions(+), 319 deletions(-) create mode 100644 flutter/lib/models/peer_tab_model.dart diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 150121c59..4080f9c11 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,7 +1,7 @@ -import 'dart:convert'; import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/common/widgets/my_group.dart'; @@ -12,181 +12,15 @@ import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:get/get.dart'; +import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; +import 'package:provider/provider.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; -const int groupTabIndex = 4; -const String defaultGroupTabname = 'Group'; - -class StatePeerTab { - final RxInt currentTab = 0.obs; // index in tabNames - final RxList visibleOrderedTabs = RxList.empty(growable: true); - List tabOrder = List.from([0, 1, 2, 3, 4]); // constant length - final RxInt tabHiddenFlag = 0.obs; - final RxList tabNames = [ - 'Recent Sessions', - 'Favorites', - 'Discovered', - 'Address Book', - defaultGroupTabname, - ].obs; - - StatePeerTab._() { - // init tabHiddenFlag - tabHiddenFlag.value = (int.tryParse( - bind.getLocalFlutterConfig(k: 'hidden-peer-card'), - radix: 2) ?? - 0); - var tabs = _notHiddenTabs(); - // remove dynamic tabs - tabs.remove(groupTabIndex); - // init tabOrder - try { - final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); - if (conf.isNotEmpty) { - final json = jsonDecode(conf); - if (json is List) { - final List list = - json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); - if (list.length == tabOrder.length && - tabOrder.every((e) => list.contains(e))) { - tabOrder = list; - } - } - } - } catch (e) { - debugPrintStack(label: '$e'); - } - // init visibleOrderedTabs - var tempList = tabOrder.toList(); - tempList.removeWhere((e) => !tabs.contains(e)); - visibleOrderedTabs.value = tempList; - // init currentTab - currentTab.value = - int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; - if (!tabs.contains(currentTab.value)) { - if (tabs.isNotEmpty) { - currentTab.value = tabs[0]; - } else { - currentTab.value = 0; - } - } - } - static final StatePeerTab instance = StatePeerTab._(); - - // check dynamic tabs - check() { - tabOrder2visibleOrderedTabs(); - if (visibleOrderedTabs.contains(groupTabIndex) && - int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == - groupTabIndex) { - currentTab.value = groupTabIndex; - } - if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) { - tabNames[groupTabIndex] = gFFI.userModel.groupName.value; - } else { - tabNames[groupTabIndex] = defaultGroupTabname; - } - } - - visibleOrderedTabs2TabOrder() { - var tmpTabOrder = visibleOrderedTabs.toList(); - var left = tabOrder.where((e) => !tmpTabOrder.contains(e)).toList(); - for (var t in left) { - _addTabInOrder(tmpTabOrder, t); - } - statePeerTab.tabOrder = tmpTabOrder; - bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder)); - } - - tabOrder2visibleOrderedTabs() { - var visible = statePeerTab.visibleTabs(); - statePeerTab.visibleOrderedTabs.value = - statePeerTab.tabOrder.where((e) => visible.contains(e)).toList(); - } - - // return true if hide group card - bool filterGroupCard() { - if (gFFI.groupModel.users.isEmpty || - (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { - return true; - } else { - return false; - } - } - - // return index array of tabNames - List visibleTabs() { - var v = List.empty(growable: true); - for (int i = 0; i < tabNames.length; i++) { - if (!_isTabHidden(i) && !_isTabFilter(i)) { - v.add(i); - } - } - return v; - } - - bool _isTabHidden(int tabindex) { - return tabHiddenFlag & (1 << tabindex) != 0; - } - - bool _isTabFilter(int tabIndex) { - if (tabIndex == groupTabIndex) { - return filterGroupCard(); - } - return false; - } - - List _notHiddenTabs() { - var v = List.empty(growable: true); - for (int i = 0; i < tabNames.length; i++) { - if (!_isTabHidden(i)) { - v.add(i); - } - } - return v; - } - - // add tabIndex to list - _addTabInOrder(List list, int tabIndex) { - if (!tabOrder.contains(tabIndex) || list.contains(tabIndex)) { - return; - } - bool sameOrder = true; - int lastIndex = -1; - for (int i = 0; i < list.length; i++) { - var index = tabOrder.lastIndexOf(list[i]); - if (index > lastIndex) { - lastIndex = index; - continue; - } else { - sameOrder = false; - break; - } - } - if (sameOrder) { - var indexInTabOrder = tabOrder.indexOf(tabIndex); - var left = List.empty(growable: true); - for (int i = 0; i < indexInTabOrder; i++) { - left.add(tabOrder[i]); - } - int insertIndex = list.lastIndexWhere((e) => left.contains(e)); - if (insertIndex < 0) { - insertIndex = 0; - } else { - insertIndex += 1; - } - list.insert(insertIndex, tabIndex); - } else { - list.add(tabIndex); - } - } -} - -final statePeerTab = StatePeerTab.instance; - class PeerTabPage extends StatefulWidget { const PeerTabPage({Key? key}) : super(key: key); @override @@ -232,11 +66,10 @@ class _PeerTabPageState extends State ), () => {}), ]; + final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50)); @override void initState() { - adjustTab(); - final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); if (uiType != '') { peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index @@ -248,7 +81,7 @@ class _PeerTabPageState extends State Future handleTabSelection(int tabIndex) async { if (tabIndex < entries.length) { - statePeerTab.currentTab.value = tabIndex; + gFFI.peerTabModel.setCurrentTab(tabIndex); entries[tabIndex].load(); } } @@ -270,6 +103,7 @@ class _PeerTabPageState extends State Expanded( child: visibleContextMenuListener( _createSwitchBar(context))), + buildScrollJumper(), const PeerSearchBar(), Offstage( offstage: !isDesktop, @@ -284,98 +118,115 @@ class _PeerTabPageState extends State } Widget _createSwitchBar(BuildContext context) { - return Obx(() { - var tabs = statePeerTab.visibleOrderedTabs; - int indexCounter = -1; - return ReorderableListView( - buildDefaultDragHandles: false, - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - var list = tabs.toList(); - final int item = list.removeAt(oldIndex); - list.insert(newIndex, item); - tabs.value = list; - statePeerTab.visibleOrderedTabs2TabOrder(); - }, - scrollDirection: Axis.horizontal, - physics: NeverScrollableScrollPhysics(), - scrollController: ScrollController(), - children: tabs.map((t) { - indexCounter++; - return ReorderableDragStartListener( + final model = Provider.of(context); + int indexCounter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: (oldIndex, newIndex) { + model.onReorder(oldIndex, newIndex); + }, + scrollDirection: Axis.horizontal, + physics: NeverScrollableScrollPhysics(), + scrollController: model.sc, + children: model.visibleOrderedTabs.map((t) { + indexCounter++; + return ReorderableDragStartListener( + key: ValueKey(t), + index: indexCounter, + child: VisibilityDetector( key: ValueKey(t), - index: indexCounter, - child: InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: statePeerTab.currentTab.value == t - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), - ), - child: Align( - alignment: Alignment.center, - child: Text( - translatedTabname(t), - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: statePeerTab.currentTab.value == t - ? MyTheme.tabbar(context).selectedTextColor - : MyTheme.tabbar(context).unSelectedTextColor - ?..withOpacity(0.5)), - ), - )), - onTap: () async { - await handleTabSelection(t); - await bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: t.toString()); + onVisibilityChanged: (info) { + final id = (info.key as ValueKey).value; + model.setTabFullyVisible(id, info.visibleFraction > 0.99); + }, + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + if (!model.sc.canScroll) return; + _scrollDebounce.call(() { + model.sc.animateTo(model.sc.offset + e.scrollDelta.dy, + duration: Duration(milliseconds: 200), + curve: Curves.ease); + }); + } }, + child: InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: model.currentTab == t + ? Theme.of(context).backgroundColor + : null, + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + ), + child: Align( + alignment: Alignment.center, + child: Text( + model.translatedTabname(t), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: model.currentTab == t + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor + ?..withOpacity(0.5)), + ), + )), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, + ), ), - ); - }).toList()); - }); + ), + ); + }).toList()); } - translatedTabname(int index) { - if (index < statePeerTab.tabNames.length) { - final name = statePeerTab.tabNames[index]; - if (index == groupTabIndex) { - if (name == defaultGroupTabname) { - return translate(name); - } else { - return name; - } - } else { - return translate(name); - } - } - assert(false); - return index.toString(); + Widget buildScrollJumper() { + final model = Provider.of(context); + return Offstage( + offstage: !model.showScrollBtn, + child: Row( + children: [ + GestureDetector( + child: Icon(Icons.arrow_left, + size: 22, + color: model.leftFullyVisible + ? Theme.of(context).disabledColor + : null), + onTap: model.sc.backward), + GestureDetector( + child: Icon(Icons.arrow_right, + size: 22, + color: model.rightFullyVisible + ? Theme.of(context).disabledColor + : null), + onTap: model.sc.forward) + ], + )); } Widget _createPeersView() { - final verticalMargin = isDesktop ? 12.0 : 6.0; - return Expanded( - child: Obx(() { - var tabs = statePeerTab.visibleOrderedTabs; - if (tabs.isEmpty) { - return visibleContextMenuListener(Center( - child: Text(translate('Right click to select tabs')), - )); + final model = Provider.of(context); + Widget child; + if (model.visibleOrderedTabs.isEmpty) { + child = visibleContextMenuListener(Center( + child: Text(translate('Right click to select tabs')), + )); + } else { + if (model.visibleOrderedTabs.contains(model.currentTab)) { + child = entries[model.currentTab].widget; } else { - if (tabs.contains(statePeerTab.currentTab.value)) { - return entries[statePeerTab.currentTab.value].widget; - } else { - statePeerTab.currentTab.value = tabs[0]; - return entries[statePeerTab.currentTab.value].widget; - } + model.setCurrentTab(model.visibleOrderedTabs[0]); + child = entries[0].widget; } - }).marginSymmetric(vertical: verticalMargin)); + } + return Expanded( + child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); } Widget _createPeerViewTypeSwitch(BuildContext context) { @@ -408,13 +259,6 @@ class _PeerTabPageState extends State ); } - adjustTab() { - var tabs = statePeerTab.visibleOrderedTabs; - if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) { - statePeerTab.currentTab.value = tabs[0]; - } - } - Widget visibleContextMenuListener(Widget child) { return Listener( onPointerDown: (e) { @@ -434,55 +278,36 @@ class _PeerTabPageState extends State } Widget visibleContextMenu(CancelFunc cancelFunc) { - return Obx(() { - final List menu = List.empty(growable: true); - final List menuIndex = List.empty(growable: true); - for (int i = 0; i < statePeerTab.tabNames.length; i++) { - if (i == groupTabIndex && statePeerTab.filterGroupCard()) { - continue; - } - int bitMask = 1 << i; - menuIndex.add(i); - menu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translatedTabname(i), - getter: () async { - return statePeerTab.tabHiddenFlag & bitMask == 0; - }, - setter: (show) async { - if (show) { - statePeerTab.tabHiddenFlag.value &= ~bitMask; - } else { - statePeerTab.tabHiddenFlag.value |= bitMask; - } - await bind.setLocalFlutterConfig( - k: 'hidden-peer-card', - v: statePeerTab.tabHiddenFlag.value.toRadixString(2)); - statePeerTab.tabOrder2visibleOrderedTabs(); - cancelFunc(); - adjustTab(); - })); - } - // show in tabOrder - List menu2 = List.empty(growable: true); - statePeerTab.tabOrder.map((e) { - final index = menuIndex.indexOf(e); - if (index >= 0) { - menu2.add(menu[index]); - } - }).toList(); - return mod_menu.PopupMenu( - items: menu2 - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: MyTheme.accent, - height: 20.0, - dividerHeight: 12.0, - ))) - .expand((i) => i) - .toList()); - }); + final model = Provider.of(context); + final List menu = List.empty(growable: true); + final List menuIndex = List.empty(growable: true); + var list = model.orderedNotFilteredTabs(); + for (int i = 0; i < list.length; i++) { + int tabIndex = list[i]; + int bitMask = 1 << tabIndex; + menuIndex.add(tabIndex); + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: model.translatedTabname(tabIndex), + getter: () async { + return model.tabHiddenFlag & bitMask == 0; + }, + setter: (show) async { + model.onHideShow(tabIndex, show); + cancelFunc(); + })); + } + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: MyTheme.accent, + height: 20.0, + dividerHeight: 12.0, + ))) + .expand((i) => i) + .toList()); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 86cc9d89b..a2ae959c0 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -353,6 +353,7 @@ class _AppState extends State { ChangeNotifierProvider.value(value: gFFI.imageModel), ChangeNotifierProvider.value(value: gFFI.cursorModel), ChangeNotifierProvider.value(value: gFFI.canvasModel), + ChangeNotifierProvider.value(value: gFFI.peerTabModel), ], child: GetMaterialApp( navigatorKey: globalKey, diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 2fbe40091..7e9a9879c 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -643,9 +643,9 @@ class _PasswordWidgetState extends State { // Here is key idea suffixIcon: IconButton( icon: Icon( - // Based on passwordVisible state choose the icon - _passwordVisible ? Icons.visibility : Icons.visibility_off, - ), + // Based on passwordVisible state choose the icon + _passwordVisible ? Icons.visibility : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), onPressed: () { // Update the state i.e. toggle the state of passwordVisible variable setState(() { diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 4d9fab0e4..5e2b85f90 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -35,7 +35,7 @@ class GroupModel { await reset(); if (gFFI.userModel.userName.isEmpty || (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); return; } userLoading.value = true; @@ -82,7 +82,7 @@ class GroupModel { userLoadError.value = err.toString(); } finally { userLoading.value = false; - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index aae4c6a07..daf7bfe34 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -13,6 +13,7 @@ import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/group_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -1292,8 +1293,9 @@ class FFI { late final AbModel abModel; // global late final GroupModel groupModel; // global late final UserModel userModel; // global + late final PeerTabModel peerTabModel; // global late final QualityMonitorModel qualityMonitorModel; // session - late final RecordingModel recordingModel; // recording + late final RecordingModel recordingModel; // session late final InputModel inputModel; // session FFI() { @@ -1305,6 +1307,7 @@ class FFI { chatModel = ChatModel(WeakReference(this)); fileModel = FileModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); + peerTabModel = PeerTabModel(WeakReference(this)); abModel = AbModel(WeakReference(this)); groupModel = GroupModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart new file mode 100644 index 000000000..7c6211682 --- /dev/null +++ b/flutter/lib/models/peer_tab_model.dart @@ -0,0 +1,275 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:scroll_pos/scroll_pos.dart'; + +import '../common.dart'; +import 'model.dart'; + +const int groupTabIndex = 4; +const String defaultGroupTabname = 'Group'; + +class PeerTabModel with ChangeNotifier { + WeakReference parent; + int get currentTab => _currentTab; + int _currentTab = 0; // index in tabNames + List get visibleOrderedTabs => _visibleOrderedTabs; + List _visibleOrderedTabs = List.empty(growable: true); + List get tabOrder => _tabOrder; + List _tabOrder = List.from([0, 1, 2, 3, 4]); // constant length + int get tabHiddenFlag => _tabHiddenFlag; + int _tabHiddenFlag = 0; + bool get showScrollBtn => _showScrollBtn; + bool _showScrollBtn = false; + final List _fullyVisible = List.filled(5, false); + bool get leftFullyVisible => _leftFullyVisible; + bool _leftFullyVisible = false; + bool get rightFullyVisible => _rightFullyVisible; + bool _rightFullyVisible = false; + ScrollPosController sc = ScrollPosController(); + List tabNames = [ + 'Recent Sessions', + 'Favorites', + 'Discovered', + 'Address Book', + defaultGroupTabname, + ]; + + PeerTabModel(this.parent) { + // init tabHiddenFlag + _tabHiddenFlag = int.tryParse( + bind.getLocalFlutterConfig(k: 'hidden-peer-card'), + radix: 2) ?? + 0; + var tabs = _notHiddenTabs(); + // remove dynamic tabs + tabs.remove(groupTabIndex); + // init tabOrder + try { + final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); + if (conf.isNotEmpty) { + final json = jsonDecode(conf); + if (json is List) { + final List list = + json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); + if (list.length == _tabOrder.length && + _tabOrder.every((e) => list.contains(e))) { + _tabOrder = list; + } + } + } + } catch (e) { + debugPrintStack(label: '$e'); + } + // init visibleOrderedTabs + var tempList = _tabOrder.toList(); + tempList.removeWhere((e) => !tabs.contains(e)); + _visibleOrderedTabs = tempList; + // init currentTab + _currentTab = + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; + if (!tabs.contains(_currentTab)) { + if (tabs.isNotEmpty) { + _currentTab = tabs[0]; + } else { + _currentTab = 0; + } + } + sc.itemCount = _visibleOrderedTabs.length; + } + + check_dynamic_tabs() { + var visible = visibleTabs(); + _visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList(); + if (_visibleOrderedTabs.contains(groupTabIndex) && + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == + groupTabIndex) { + _currentTab = groupTabIndex; + } + if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) { + tabNames[groupTabIndex] = gFFI.userModel.groupName.value; + } else { + tabNames[groupTabIndex] = defaultGroupTabname; + } + sc.itemCount = _visibleOrderedTabs.length; + notifyListeners(); + } + + setCurrentTab(int index) { + if (_currentTab != index) { + _currentTab = index; + notifyListeners(); + } + } + + setTabFullyVisible(int index, bool visible) { + if (index >= 0 && index < _fullyVisible.length) { + if (visible != _fullyVisible[index]) { + _fullyVisible[index] = visible; + bool changed = false; + bool show = _visibleOrderedTabs.any((e) => !_fullyVisible[e]); + if (show != _showScrollBtn) { + _showScrollBtn = show; + changed = true; + } + if (_visibleOrderedTabs.isNotEmpty && _visibleOrderedTabs[0] == index) { + if (_leftFullyVisible != visible) { + _leftFullyVisible = visible; + changed = true; + } + } + if (_visibleOrderedTabs.isNotEmpty && + _visibleOrderedTabs.last == index) { + if (_rightFullyVisible != visible) { + _rightFullyVisible = visible; + changed = true; + } + } + if (changed) { + notifyListeners(); + } + } + } + } + + onReorder(oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + var list = _visibleOrderedTabs.toList(); + final int item = list.removeAt(oldIndex); + list.insert(newIndex, item); + _visibleOrderedTabs = list; + + var tmpTabOrder = _visibleOrderedTabs.toList(); + var left = _tabOrder.where((e) => !tmpTabOrder.contains(e)).toList(); + for (var t in left) { + _addTabInOrder(tmpTabOrder, t); + } + _tabOrder = tmpTabOrder; + bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder)); + notifyListeners(); + } + + onHideShow(int index, bool show) async { + int bitMask = 1 << index; + if (show) { + _tabHiddenFlag &= ~bitMask; + } else { + _tabHiddenFlag |= bitMask; + } + await bind.setLocalFlutterConfig( + k: 'hidden-peer-card', v: _tabHiddenFlag.toRadixString(2)); + var visible = visibleTabs(); + _visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList(); + if (_visibleOrderedTabs.isNotEmpty && + !_visibleOrderedTabs.contains(_currentTab)) { + _currentTab = _visibleOrderedTabs[0]; + } + notifyListeners(); + } + + List orderedNotFilteredTabs() { + var list = tabOrder.toList(); + if (_filterGroupCard()) { + list.remove(groupTabIndex); + } + return list; + } + + // return index array of tabNames + List visibleTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i) && !_isTabFilter(i)) { + v.add(i); + } + } + return v; + } + + String translatedTabname(int index) { + if (index >= 0 && index < tabNames.length) { + final name = tabNames[index]; + if (index == groupTabIndex) { + if (name == defaultGroupTabname) { + return translate(name); + } else { + return name; + } + } else { + return translate(name); + } + } + assert(false); + return index.toString(); + } + + bool _isTabHidden(int tabindex) { + return _tabHiddenFlag & (1 << tabindex) != 0; + } + + bool _isTabFilter(int tabIndex) { + if (tabIndex == groupTabIndex) { + return _filterGroupCard(); + } + return false; + } + + // return true if hide group card + bool _filterGroupCard() { + if (gFFI.groupModel.users.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + return true; + } else { + return false; + } + } + + List _notHiddenTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i)) { + v.add(i); + } + } + return v; + } + + // add tabIndex to list + _addTabInOrder(List list, int tabIndex) { + if (!_tabOrder.contains(tabIndex) || list.contains(tabIndex)) { + return; + } + bool sameOrder = true; + int lastIndex = -1; + for (int i = 0; i < list.length; i++) { + var index = _tabOrder.lastIndexOf(list[i]); + if (index > lastIndex) { + lastIndex = index; + continue; + } else { + sameOrder = false; + break; + } + } + if (sameOrder) { + var indexInTabOrder = _tabOrder.indexOf(tabIndex); + var left = List.empty(growable: true); + for (int i = 0; i < indexInTabOrder; i++) { + left.add(_tabOrder[i]); + } + int insertIndex = list.lastIndexWhere((e) => left.contains(e)); + if (insertIndex < 0) { + insertIndex = 0; + } else { + insertIndex += 1; + } + list.insert(insertIndex, tabIndex); + } else { + list.add(tabIndex); + } + } +} diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 6694d8c5c..7f40b3333 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -62,7 +62,7 @@ class UserModel { await gFFI.groupModel.reset(); userName.value = ''; groupName.value = ''; - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); } Future _parseAndUpdateUser(UserPayload user) async { From 893f18cdec1b4fedf72af67bbfb7fe03e047db11 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 7 Feb 2023 00:11:48 +0900 Subject: [PATCH 332/734] add PenetrableOverlayState, opt chat page over remote_page --- flutter/lib/common/widgets/overlay.dart | 106 ++++++++++++++---- flutter/lib/desktop/pages/remote_page.dart | 89 ++++++++------- .../lib/desktop/widgets/remote_menubar.dart | 13 ++- flutter/lib/models/chat_model.dart | 75 +++++-------- 4 files changed, 177 insertions(+), 106 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index d84789d9c..3e248700f 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,7 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import '../../consts.dart'; @@ -92,31 +92,30 @@ class DraggableChatWindow extends StatelessWidget { bottom: BorderSide( color: Theme.of(context).hintColor.withOpacity(0.4)))), height: 38, - child: Obx(() => Opacity( - opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, child: Row(children: [ Icon(Icons.chat_bubble_outline, size: 20, color: Theme.of(context).colorScheme.primary), SizedBox(width: 6), Text(translate("Chat")) - ])), - Padding( - padding: EdgeInsets.all(2), - child: ActionIcon( - message: 'Close', - icon: IconFont.close, - onTap: chatModel.hideChatWindowOverlay, - isClose: true, - boxSize: 32, - )) - ], - ))), + ])))), + Padding( + padding: EdgeInsets.all(2), + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ), ); } } @@ -372,3 +371,68 @@ class QualityMonitor extends StatelessWidget { ) : const SizedBox.shrink())); } + +class PenetrableOverlayState { + final _middleBlocked = false.obs; + final _overlayKey = GlobalKey(); + + VoidCallback? onMiddleBlockedClick; // to-do use listener + + RxBool get middleBlocked => _middleBlocked; + GlobalKey get overlayKey => _overlayKey; + OverlayState? get overlayState => _overlayKey.currentState; + + OverlayState? getOverlayStateOrGlobal() { + if (overlayState == null) { + if (globalKey.currentState == null || + globalKey.currentState!.overlay == null) return null; + return globalKey.currentState!.overlay; + } else { + return overlayState; + } + } + + void addMiddleBlockedListener(void Function(bool) cb) { + _middleBlocked.listen(cb); + } + + void setMiddleBlocked(bool blocked) { + if (blocked != _middleBlocked.value) { + _middleBlocked.value = blocked; + } + } +} + +class PenetrableOverlay extends StatelessWidget { + final Widget underlying; + final List? upperLayer; + + final PenetrableOverlayState state; + + PenetrableOverlay( + {required this.underlying, required this.state, this.upperLayer}); + + @override + Widget build(BuildContext context) { + final initialEntries = [ + OverlayEntry(builder: (_) => underlying), + + /// middle layer + OverlayEntry( + builder: (context) => Obx(() => Listener( + onPointerDown: (_) { + state.onMiddleBlockedClick?.call(); + }, + child: Container( + color: state.middleBlocked.value + ? Colors.red.withOpacity(0.3) + : null)))), + ]; + + if (upperLayer != null) { + initialEntries.addAll(upperLayer!); + } + + return Overlay(key: state.overlayKey, initialEntries: initialEntries); + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2e4668159..4bda68c2d 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -62,6 +62,8 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; + final overlayState = PenetrableOverlayState(); + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -133,6 +135,12 @@ class _RemotePageState extends State // }); // _isCustomCursorInited = true; // } + + _ffi.chatModel.setPenetrableOverlayState(overlayState); + // make remote page penetrable automatically, effective for chat over remote + overlayState.onMiddleBlockedClick = () { + overlayState.setMiddleBlocked(false); + }; } @override @@ -192,39 +200,47 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.chatModel.setOverlayState(Overlay.of(context)); - _ffi.dialogManager.setOverlayState(Overlay.of(context)); - return Container( - color: Colors.black, - child: RawKeyFocusScope( - focusNode: _rawKeyFocusNode, - onFocusChange: (bool imageFocused) { - debugPrint( - "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); - // See [onWindowBlur]. - if (Platform.isWindows) { - if (_isWindowBlur) { - imageFocused = false; - Future.delayed(Duration.zero, () { - _rawKeyFocusNode.unfocus(); - }); - } - if (imageFocused) { - _ffi.inputModel.enterOrLeave(true); - } else { - _ffi.inputModel.enterOrLeave(false); - } - } - }, - inputModel: _ffi.inputModel, - child: getBodyForDesktop(context))); - }) - ], - )); + backgroundColor: Theme.of(context).backgroundColor, + body: PenetrableOverlay( + state: overlayState, + underlying: Container( + color: Colors.black, + child: RawKeyFocusScope( + focusNode: _rawKeyFocusNode, + onFocusChange: (bool imageFocused) { + debugPrint( + "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); + // See [onWindowBlur]. + if (Platform.isWindows) { + if (_isWindowBlur) { + imageFocused = false; + Future.delayed(Duration.zero, () { + _rawKeyFocusNode.unfocus(); + }); + } + if (imageFocused) { + _ffi.inputModel.enterOrLeave(true); + } else { + _ffi.inputModel.enterOrLeave(false); + } + } + }, + inputModel: _ffi.inputModel, + child: getBodyForDesktop(context))), + upperLayer: [ + OverlayEntry( + builder: (context) => RemoteMenubar( + id: widget.id, + ffi: _ffi, + state: widget.menubarState, + onEnterOrLeaveImageSetter: (func) => + _onEnterOrLeaveImage4Menubar = func, + onEnterOrLeaveImageCleaner: () => + _onEnterOrLeaveImage4Menubar = null, + )) + ], + ), + ); } @override @@ -345,13 +361,6 @@ class _RemotePageState extends State QualityMonitor(_ffi.qualityMonitorModel), null, null), ), ); - paints.add(RemoteMenubar( - id: widget.id, - ffi: _ffi, - state: widget.menubarState, - onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, - onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, - )); return Stack( children: paints, ); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 64d289fcc..6ad030464 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -297,12 +297,23 @@ class _RemoteMenubarState extends State { ); } + final _chatButtonKey = GlobalKey(); Widget _buildChat(BuildContext context) { return IconButton( + key: _chatButtonKey, tooltip: translate('Chat'), 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(); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); }, icon: const Icon( Icons.message, diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index dd35bd22f..b61ce79a7 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -5,7 +5,6 @@ import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; -import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -30,16 +29,12 @@ class MessageBody { class ChatModel with ChangeNotifier { static final clientModeID = -1; - /// _overlayState: - /// Desktop: store session overlay by using [setOverlayState]. - /// Mobile: always null, use global overlay. - /// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay] - OverlayState? _overlayState; OverlayEntry? chatIconOverlayEntry; OverlayEntry? chatWindowOverlayEntry; bool isConnManager = false; RxBool isWindowFocus = true.obs; + PenetrableOverlayState? pOverlayState; final ChatUser me = ChatUser( id: "", @@ -58,6 +53,19 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; + void setPenetrableOverlayState(PenetrableOverlayState state) { + pOverlayState = state; + + pOverlayState!.addMiddleBlockedListener((v) { + if (!v) { + isWindowFocus.value = false; + if (isWindowFocus.value) { + isWindowFocus.toggle(); + } + } + }); + } + final WeakReference parent; ChatModel(this.parent); @@ -74,20 +82,6 @@ class ChatModel with ChangeNotifier { } } - setOverlayState(OverlayState? os) { - _overlayState = os; - } - - OverlayState? _getOverlayState() { - if (_overlayState == null) { - if (globalKey.currentState == null || - globalKey.currentState!.overlay == null) return null; - return globalKey.currentState!.overlay; - } else { - return _overlayState; - } - } - showChatIconOverlay({Offset offset = const Offset(200, 50)}) { if (chatIconOverlayEntry != null) { chatIconOverlayEntry!.remove(); @@ -100,7 +94,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = _getOverlayState(); + final overlayState = pOverlayState?.getOverlayStateOrGlobal(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -132,33 +126,26 @@ class ChatModel with ChangeNotifier { } } - showChatWindowOverlay() { + showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; - final overlayState = _getOverlayState(); + isWindowFocus.value = true; + pOverlayState?.setMiddleBlocked(true); + + final overlayState = pOverlayState?.getOverlayStateOrGlobal(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - bool innerClicked = false; return Listener( onPointerDown: (_) { - if (!innerClicked) { - isWindowFocus.value = false; + if (!isWindowFocus.value) { + isWindowFocus.value = true; + pOverlayState?.setMiddleBlocked(true); } - innerClicked = false; }, - child: Obx(() => Container( - color: isWindowFocus.value ? Colors.red.withOpacity(0.3) : null, - child: Listener( - onPointerDown: (_) { - innerClicked = true; - if (!isWindowFocus.value) { - isWindowFocus.value = true; - } - }, - child: DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this))))); + child: DraggableChatWindow( + position: chatInitPos ?? Offset(20, 80), + width: 250, + height: 350, + chatModel: this)); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; @@ -167,6 +154,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { + pOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; @@ -176,13 +164,13 @@ class ChatModel with ChangeNotifier { _isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || chatWindowOverlayEntry == null); - toggleChatOverlay() { + toggleChatOverlay({Offset? chatInitPos}) { if (_isChatOverlayHide()) { gFFI.invokeMethod("enable_soft_keyboard", true); if (!isDesktop) { showChatIconOverlay(); } - showChatWindowOverlay(); + showChatWindowOverlay(chatInitPos: chatInitPos); } else { hideChatIconOverlay(); hideChatWindowOverlay(); @@ -310,7 +298,6 @@ class ChatModel with ChangeNotifier { close() { hideChatIconOverlay(); hideChatWindowOverlay(); - _overlayState = null; notifyListeners(); } From b2afde4b27e82944250143304529210e6d6ad5aa Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 00:18:25 +0800 Subject: [PATCH 333/734] tmp workaround of '-cm' not exit cause rustdesk not launchable from finder --- src/flutter.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/flutter.rs b/src/flutter.rs index d8f83c6b2..761f8a612 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -39,7 +39,8 @@ pub extern "C" fn rustdesk_core_main() -> bool { #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { hbb_common::log::debug!("icon clicked on finder"); - if std::env::args().nth(1) == Some("--server".to_owned()) { + let x = std::env::args().nth(1).unwrap_or_default(); + if x == "--server" || x == "--cm" { crate::platform::macos::check_main_window(); } } From 1426771ec9cb208ec5d5e06fe643ea2bf0852f1d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 01:31:11 +0800 Subject: [PATCH 334/734] fix: uni links failed to be invoked with --cm running on macOS --- flutter/lib/common.dart | 16 +++++++++++++--- flutter/lib/main.dart | 8 +++++++- flutter/lib/models/native_model.dart | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8236597ff..41043069a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1292,14 +1292,24 @@ Future initUniLinks() async { } } -StreamSubscription? listenUniLinks() { - if (!(Platform.isWindows || Platform.isMacOS)) { +/// Listen for uni links. +/// +/// * handleByFlutter: Should uni links being handled by Flutter. +/// +/// Returns a [StreamSubscription] which can listen the uni links. +StreamSubscription? listenUniLinks({handleByFlutter = true}) { + if (Platform.isLinux) { return null; } final sub = uriLinkStream.listen((Uri? uri) { + debugPrint("A uri was received: $uri."); if (uri != null) { - callUniLinksUriHandler(uri); + if (handleByFlutter) { + callUniLinksUriHandler(uri); + } else { + bind.sendUrlScheme(url: uri.toString()); + } } else { print("uni listen error: uri is empty."); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index a2ae959c0..cc40d962f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -31,6 +32,9 @@ int? kWindowId; WindowType? kWindowType; late List kBootArgs; +/// Uni links. +StreamSubscription? _uniLinkSubscription; + Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); @@ -203,7 +207,7 @@ void runMultiWindow( await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; default: - // no such appType + // no such appType exit(0); } // show window from hidden status @@ -222,6 +226,8 @@ void runConnectionManagerScreen(bool hide) async { } else { showCmWindow(); } + // Start the uni links handler and redirect links to Native, not for Flutter. + _uniLinkSubscription = listenUniLinks(handleByFlutter: false); } void showCmWindow() { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index d6885bfb0..628bf502d 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -117,7 +117,7 @@ class PlatformFFI { if (Platform.isLinux) { // Start a dbus service, no need to await _ffiBind.mainStartDbusServer(); - } else if (Platform.isMacOS) { + } else if (Platform.isMacOS && isMain) { // Start an ipc server for handling url schemes. _ffiBind.mainStartIpcUrlServer(); } From 9d391d3801b802b5885ed1ab35af9bb01670e07c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 01:35:38 +0800 Subject: [PATCH 335/734] opt: format and name --- flutter/lib/common.dart | 2 +- flutter/lib/main.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 41043069a..30d38b8db 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1294,7 +1294,7 @@ Future initUniLinks() async { /// Listen for uni links. /// -/// * handleByFlutter: Should uni links being handled by Flutter. +/// * handleByFlutter: Should uni links be handled by Flutter. /// /// Returns a [StreamSubscription] which can listen the uni links. StreamSubscription? listenUniLinks({handleByFlutter = true}) { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index cc40d962f..c19adf753 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -207,7 +207,7 @@ void runMultiWindow( await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; default: - // no such appType + // no such appType exit(0); } // show window from hidden status From 564b35d4c2a0bd9266c8b76f3eb6f6d6293e2a41 Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Mon, 6 Feb 2023 21:29:36 +0100 Subject: [PATCH 336/734] Update pl.rs --- src/lang/pl.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index daf4a7846..b7ccbdbb1 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -270,8 +270,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OPEN", "Otwórz"), ("Chat", "Czat"), ("Total", "Łącznie"), - ("items", "elementy"), - ("Selected", "Zaznaczone"), + ("items", "elementów"), + ("Selected", "Zaznaczonych"), ("Screen Capture", "Przechwytywanie ekranu"), ("Input Control", "Kontrola wejścia"), ("Audio Capture", "Przechwytywanie dźwięku"), @@ -345,7 +345,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark Theme", "Ciemny motyw"), ("Dark", "Ciemny"), ("Light", "Jasny"), - ("Follow System", "Zgodne z systemem"), + ("Follow System", "Zgodny z systemem"), ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), ("Enable Audio", "Włącz dźwięk"), @@ -392,7 +392,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "albo"), ("Continue with", "Kontynuuj z"), ("Elevate", "Uzyskaj uprawnienia"), - ("Zoom cursor", "Zoom kursora"), + ("Zoom cursor", "Powiększenie kursora"), ("Accept sessions via password", "Uwierzytelnij sesję używając hasła"), ("Accept sessions via click", "Uwierzytelnij sesję poprzez kliknięcie"), ("Accept sessions via both", "Uwierzytelnij sesję za pomocą obu sposobów"), @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Right click to select tabs", "Kliknij prawym przyciskiem myszy by wybrać zakładkę"), ("Skipped", "Pominięte"), ("Add to Address Book", "Dodaj do Książki Adresowej"), - ("Group", "Grypy"), + ("Group", "Grupy"), ("Search", "Szukaj"), ("Closed manually by web console", "Zakończone manualnie z konsoli Web"), ("Local keyboard type", "Lokalny typ klawiatury"), @@ -433,17 +433,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Słabe"), ("Medium", "Średnie"), ("Strong", "Mocne"), - ("Switch Sides", "Zmień Strony"), + ("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", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Wyświetlanie"), + ("Default View Style", "Domyślny styl wyświetlania"), + ("Default Scroll Style", "Domyślny styl przewijania"), + ("Default Image Quality", "Domyślna jakość obrazu"), + ("Default Codec", "Dokyślny kodek"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Inne opcje domyślne"), ].iter().cloned().collect(); } From bcbc1573aa9f13114987dba9015a411f08fb8d59 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Mon, 6 Feb 2023 22:45:27 +0100 Subject: [PATCH 337/734] Update fr.rs --- src/lang/fr.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 19b932d2f..3b7f23ab9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -63,7 +63,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Skip", "Ignorer"), ("Close", "Fermer"), ("Retry", "Réessayer"), - ("OK", "Confirmer"), + ("OK", "Valider"), ("Password Required", "Mot de passe requis"), ("Please enter your password", "Veuillez saisir votre mot de passe"), ("Remember password", "Mémoriser le mot de passe"), @@ -126,7 +126,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afficher le moniteur de qualité"), ("Disable clipboard", "Désactiver le presse-papier"), ("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"), - ("Insert", "Insérer"), + ("Insert", "Envoyer"), ("Insert Lock", "Verrouiller l'ordinateur distant"), ("Refresh", "Rafraîchir l'écran"), ("ID does not exist", "L'ID n'existe pas"), @@ -291,7 +291,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Overwrite", "Écraser"), ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), ("Quit", "Quitter"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("doc_mac_permission", "https://rustdesk.com/docs/fr/manual/mac/#enable-permissions"), ("Help", "Aider"), ("Failed", "échouer"), ("Succeeded", "Succès"), @@ -435,15 +435,15 @@ 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", ""), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("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"), + ("Default Image Quality", "Qualité d'image par défaut"), + ("Default Codec", "Codec par défaut"), + ("Bitrate", "Débit"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Autres options par défaut"), ].iter().cloned().collect(); } From e1a9cfcf7f841e57715709f8adcd6407a2e1062b Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 7 Feb 2023 12:47:07 +0800 Subject: [PATCH 338/734] fix flink Signed-off-by: 21pages --- src/server/portable_service.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index a2f6fb829..c783fef52 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -118,11 +118,9 @@ impl SharedMemory { fn flink(name: String) -> ResultType { let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); - let mut dir = PathBuf::from(disk); - let dir1 = dir.join("ProgramData"); - let dir2 = std::env::var("TEMP") - .map(|d| PathBuf::from(d)) - .unwrap_or(dir.join("Windows").join("Temp")); + let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); + let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); + let mut dir; if dir1.exists() { dir = dir1; } else if dir2.exists() { From cf3ddb2a183bcfdd506942fde57f69c36058756b Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 7 Feb 2023 15:16:49 +0800 Subject: [PATCH 339/734] filter foreground window to avoid frequent prompts Signed-off-by: 21pages --- src/platform/windows.rs | 31 ++++++++++++++++++++++++++++++- src/server/video_service.rs | 5 +---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 2e0d56eab..17f275c2a 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -11,6 +11,7 @@ use std::io::prelude::*; use std::{ ffi::OsString, fs, io, mem, + os::windows::process::CommandExt, path::PathBuf, sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -1644,6 +1645,29 @@ pub fn is_elevated(process_id: Option) -> ResultType { } } +#[inline] +fn filter_foreground_window(process_id: DWORD) -> ResultType { + if let Ok(output) = std::process::Command::new("tasklist") + .args(vec![ + "/SVC", + "/NH", + "/FI", + &format!("PID eq {}", process_id), + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + { + let s = String::from_utf8_lossy(&output.stdout) + .to_string() + .to_lowercase(); + Ok(["Taskmgr", "mmc", "regedit"] + .iter() + .any(|name| s.contains(&name.to_string().to_lowercase()))) + } else { + bail!("run tasklist failed"); + } +} + pub fn is_foreground_window_elevated() -> ResultType { unsafe { let mut process_id: DWORD = 0; @@ -1651,7 +1675,12 @@ pub fn is_foreground_window_elevated() -> ResultType { if process_id == 0 { bail!("Failed to get processId, errno {}", GetLastError()) } - is_elevated(Some(process_id)) + let elevated = is_elevated(Some(process_id))?; + if elevated { + filter_foreground_window(process_id) + } else { + Ok(false) + } } } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 55920e320..57fdf2c22 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -954,10 +954,7 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> { fn start_uac_elevation_check() { static START: Once = Once::new(); START.call_once(|| { - if !crate::platform::is_installed() - && !crate::platform::is_root() - && !crate::portable_service::client::running() - { + if !crate::platform::is_installed() && !crate::platform::is_root() { std::thread::spawn(|| loop { std::thread::sleep(std::time::Duration::from_secs(1)); if let Ok(uac) = crate::ui::win_privacy::is_process_consent_running() { From 8aba51c1202e45067d1fc2a541cc096fccf5d4d4 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 7 Feb 2023 15:39:46 +0800 Subject: [PATCH 340/734] fix cm push_event Signed-off-by: 21pages --- src/flutter.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 761f8a612..b4f1f6bc6 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -14,6 +14,7 @@ use std::{ }; pub(super) const APP_TYPE_MAIN: &str = "main"; +pub(super) const APP_TYPE_CM: &str = "cm"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; pub(super) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; @@ -528,11 +529,7 @@ pub mod connection_manager { assert!(h.get("name").is_none()); h.insert("name", name); - if let Some(s) = GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(super::APP_TYPE_MAIN) - { + 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())); }; } From e0f73ccc28e2f85cb999d03281f29d2fdaf6280d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 16:17:00 +0800 Subject: [PATCH 341/734] remove docs/SECURITY.md to disable security report which is not the report we imagine --- docs/SECURITY.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 docs/SECURITY.md diff --git a/docs/SECURITY.md b/docs/SECURITY.md deleted file mode 100644 index f1114f913..000000000 --- a/docs/SECURITY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| --------- | ------------------ | -| 1.1.x | :white_check_mark: | -| 1.x | :white_check_mark: | -| Below 1.0 | :x: | - -## Reporting a Vulnerability - -Here we should write what to do in case of a security vulnerability From 28ad271693c4ba550d41b82a68a7a90d392118dc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 3 Nov 2022 21:09:37 +0800 Subject: [PATCH 342/734] wip: dual audio transmission server --- src/client/io_loop.rs | 54 +++++++++++++++++++++++++++++++++++++++- src/server.rs | 21 ++++++++++++++++ src/server/connection.rs | 6 +++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 0178fe9e8..bcbea994b 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,13 +2,16 @@ use crate::client::{ Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; -use crate::common; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; +use hbb_common::futures::channel::mpsc::unbounded; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +use crate::server::Service; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::Data, client::Interface}; @@ -253,6 +256,55 @@ impl Remote { } } + // Start a local audio recorder, records audio and send to remote + fn start_client_audio( + &mut self, + audio_sender: MediaSender, + ) -> Option> { + if self.handler.is_file_transfer() || self.handler.is_port_forward() { + return None; + } + // Create a channel to receive error or closed message + let (tx, rx) = std::sync::mpsc::channel(); + let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); + // Create a stand-alone inner, add subscribe to audio service + let client_conn_inner = ConnInner::new( + CLIENT_SERVER.write().unwrap().get_new_id(), + Some(tx_audio_data), + None, + ); + CLIENT_SERVER + .write() + .unwrap() + .subscribe(audio_service::NAME, client_conn_inner, true); + std::thread::spawn(move || { + loop { + // check if client is closed + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit local audio service of client"); + break; + } + _ => {} + } + match rx_audio_data.try_recv() { + Ok((instant, msg)) => match msg.union { + Some(_) => todo!(), + None => todo!(), + }, + Err(err) => { + if err == TryRecvError::Empty { + // ignore + } else { + log::debug!("Failed to record local audio channel: {}", err); + } + } + } + } + }); + Some(tx) + } + fn start_clipboard(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; diff --git a/src/server.rs b/src/server.rs index 109fc1e9a..bef49f132 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,6 +29,13 @@ use service::{GenericService, Service, Subscriber}; use service::ServiceTmpl; use crate::ipc::{connect, Data}; +pub use service::{GenericService, Service, ServiceTmpl, Subscriber}; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; pub mod audio_service; cfg_if::cfg_if! { @@ -65,6 +72,13 @@ type ConnMap = HashMap; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CONN_COUNT: Arc> = Default::default(); + // A client server used to provide local services(audio, video, clipboard, etc.) + // for all initiative connections. + // + // [Note] + // Now we use this [`CLIENT_SERVER`] to do following operations: + // - record local audio, and send to remote + pub static ref CLIENT_SERVER: ServerPtr = new(); } pub struct Server { @@ -316,6 +330,13 @@ impl Server { } } } + + // get a new unique id + pub fn get_new_id(&mut self) -> i32 { + let new_id = self.id_count; + self.id_count += 1; + new_id + } } impl Drop for Server { diff --git a/src/server/connection.rs b/src/server/connection.rs index e4b667d54..d340021ad 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -108,6 +108,12 @@ pub struct Connection { from_switch: bool, } +impl ConnInner { + pub fn new(id: i32, tx: Option, tx_video: Option) -> Self { + Self { id, tx, tx_video } + } +} + impl Subscriber for ConnInner { #[inline] fn id(&self) -> i32 { From 1f40963b5d23fd4cc6c7be75aa55077b977ed5f0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 4 Nov 2022 12:02:17 +0800 Subject: [PATCH 343/734] wip: connection --- src/client.rs | 16 ++++++++++--- src/client/io_loop.rs | 51 ++++++++++++++++++++++++++++------------ src/flutter_ffi.rs | 3 +++ src/server/connection.rs | 8 +++++++ 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/client.rs b/src/client.rs index e0ac68c5d..08a8de747 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1543,7 +1543,6 @@ where F: 'static + FnMut(&[u8]) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); - let (audio_sender, audio_receiver) = mpsc::channel::(); let mut video_callback = video_callback; let latency_controller = LatencyController::new(); @@ -1573,8 +1572,19 @@ where } log::info!("Video decoder loop exits"); }); + let audio_sender = start_audio_thread(Some(latency_controller_cl)); + return (video_sender, audio_sender); +} + +/// Start an audio thread +/// Return a audio [`MediaSender`] +pub fn start_audio_thread( + latency_controller: Option>>, +) -> MediaSender { + let latency_controller = latency_controller.unwrap_or(LatencyController::new()); + let (audio_sender, audio_receiver) = mpsc::channel::(); std::thread::spawn(move || { - let mut audio_handler = AudioHandler::new(latency_controller_cl); + let mut audio_handler = AudioHandler::new(latency_controller); loop { if let Ok(data) = audio_receiver.recv() { match data { @@ -1592,7 +1602,7 @@ where } log::info!("Audio decoder loop exits"); }); - return (video_sender, audio_sender); + audio_sender } /// Handle latency test. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bcbea994b..857f94891 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -32,6 +32,7 @@ use hbb_common::tokio::{ }; use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; +use std::borrow::Borrow; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -89,6 +90,7 @@ impl Remote { pub async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); + let stop_client_audio = self.start_client_audio(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -96,6 +98,7 @@ impl Remote { } else { ConnType::default() }; + match Client::start( &self.handler.id, key, @@ -224,6 +227,9 @@ impl Remote { if let Some(stop) = stop_clipboard { stop.send(()).ok(); } + if let Some(stop) = stop_client_audio { + 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); @@ -257,10 +263,7 @@ impl Remote { } // Start a local audio recorder, records audio and send to remote - fn start_client_audio( - &mut self, - audio_sender: MediaSender, - ) -> Option> { + fn start_client_audio(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } @@ -268,29 +271,47 @@ impl Remote { let (tx, rx) = std::sync::mpsc::channel(); let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); // Create a stand-alone inner, add subscribe to audio service - let client_conn_inner = ConnInner::new( - CLIENT_SERVER.write().unwrap().get_new_id(), - Some(tx_audio_data), - None, + let conn_id = CLIENT_SERVER.write().unwrap().get_new_id(); + let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None); + // now we subscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner.clone(), + true, ); - CLIENT_SERVER - .write() - .unwrap() - .subscribe(audio_service::NAME, client_conn_inner, true); + let tx_audio = self.sender.clone(); std::thread::spawn(move || { loop { // check if client is closed match rx.try_recv() { Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { log::debug!("Exit local audio service of client"); + // unsubscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner, + false, + ); break; } _ => {} } match rx_audio_data.try_recv() { - Ok((instant, msg)) => match msg.union { - Some(_) => todo!(), - None => todo!(), + Ok((instant, msg)) => match &msg.union { + Some(message::Union::AudioFrame(frame)) => { + let mut msg = Message::new(); + msg.set_audio_frame(frame.clone()); + tx_audio.send(Data::Message(msg)).ok(); + log::debug!("send audio frame {}", frame.timestamp); + } + Some(message::Union::Misc(misc)) => { + let mut msg = Message::new(); + msg.set_misc(misc.clone()); + tx_audio.send(Data::Message(msg)).ok(); + log::debug!("send audio misc {:?}", misc.audio_format()); + } + _ => {} + None => {} }, Err(err) => { if err == TryRecvError::Empty { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ca9314c43..4b671ff1b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1244,6 +1244,9 @@ pub fn main_current_is_wayland() -> SyncReturn { pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) +pub fn main_start_pa() { + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { diff --git a/src/server/connection.rs b/src/server/connection.rs index d340021ad..34adeb59b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1533,6 +1533,10 @@ impl Connection { } _ => {} }, + Some(misc::Union::AudioFormat(format)) => { + // TODO: implement audio format handler + println!("recv audio format"); + } #[cfg(feature = "flutter")] Some(misc::Union::SwitchSidesRequest(s)) => { if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { @@ -1550,6 +1554,10 @@ impl Connection { } _ => {} }, + Some(message::Union::AudioFrame(audio_frame)) => { + // TODO: implement audio frame handler + println!("recv audio frame"); + } _ => {} } } From 65ab43aa4a9ebc7563a1eceb822c57f309d24eb3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 17 Dec 2022 10:39:07 +0800 Subject: [PATCH 344/734] opt: compile --- src/flutter_ffi.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4b671ff1b..d9f67e566 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1244,6 +1244,8 @@ pub fn main_current_is_wayland() -> SyncReturn { pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) +} + pub fn main_start_pa() { #[cfg(target_os = "linux")] std::thread::spawn(crate::ipc::start_pa); From 8e2d6945d0e0b3fd16de4d7b8883867c3236a4c8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 11:55:37 +0800 Subject: [PATCH 345/734] feat: add audio thread in server being controlled --- src/client/io_loop.rs | 3 +-- src/server/connection.rs | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 857f94891..bac1e5d23 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -297,7 +297,7 @@ impl Remote { _ => {} } match rx_audio_data.try_recv() { - Ok((instant, msg)) => match &msg.union { + Ok((_instant, msg)) => match &msg.union { Some(message::Union::AudioFrame(frame)) => { let mut msg = Message::new(); msg.set_audio_frame(frame.clone()); @@ -311,7 +311,6 @@ impl Remote { log::debug!("send audio misc {:?}", misc.audio_format()); } _ => {} - None => {} }, Err(err) => { if err == TryRecvError::Empty { diff --git a/src/server/connection.rs b/src/server/connection.rs index 34adeb59b..2ee3bc8ed 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,7 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::video_service; +use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -95,6 +95,7 @@ pub struct Connection { disable_clipboard: bool, // by peer disable_audio: bool, // by peer enable_file_transfer: bool, // by peer + audio_sender: MediaSender, // audio by the remote peer/client tx_input: std_mpsc::Sender, // handle input messages video_ack_required: bool, peer_info: (String, String), @@ -168,6 +169,9 @@ impl Connection { let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); + // Start a audio thread to play the audio sent by peer. + let latency_controller = LatencyController::new(); + let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { id, @@ -209,6 +213,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, + audio_sender, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -1534,8 +1539,9 @@ impl Connection { _ => {} }, Some(misc::Union::AudioFormat(format)) => { - // TODO: implement audio format handler - println!("recv audio format"); + if !self.disable_audio { + allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); + } } #[cfg(feature = "flutter")] Some(misc::Union::SwitchSidesRequest(s)) => { @@ -1554,9 +1560,10 @@ impl Connection { } _ => {} }, - Some(message::Union::AudioFrame(audio_frame)) => { - // TODO: implement audio frame handler - println!("recv audio frame"); + Some(message::Union::AudioFrame(frame)) => { + if !self.disable_audio { + allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); + } } _ => {} } From 45a6fc361883a6fd1ff76a4fe3a7ca9bd54b09da Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 14:10:06 +0800 Subject: [PATCH 346/734] opt: remove latency detector on single audio --- src/client.rs | 5 +++++ src/client/helper.rs | 11 +++++++++++ src/server/connection.rs | 4 +++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 08a8de747..b2cd0f2f7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -714,6 +714,7 @@ impl AudioHandler { .check_audio(frame.timestamp) .not() { + log::debug!("audio frame {} is ignored", frame.timestamp); return; } } @@ -724,6 +725,7 @@ impl AudioHandler { } #[cfg(target_os = "linux")] if self.simple.is_none() { + log::debug!("PulseAudio simple binding does not exists"); return; } #[cfg(target_os = "android")] @@ -768,6 +770,7 @@ impl AudioHandler { unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; self.simple.as_mut().map(|x| x.write(data_u8)); } + log::debug!("write Audio frame {} to system.", frame.timestamp); } }); } @@ -1589,9 +1592,11 @@ pub fn start_audio_thread( if let Ok(data) = audio_receiver.recv() { match data { MediaData::AudioFrame(af) => { + log::debug!("recved audio frame={}", af.timestamp); audio_handler.handle_frame(af); } MediaData::AudioFormat(f) => { + log::debug!("recved audio format, sample rate={}", f.sample_rate); audio_handler.handle_format(f); } _ => {} diff --git a/src/client/helper.rs b/src/client/helper.rs index e4736c0e8..005b2df72 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -18,6 +18,7 @@ pub struct LatencyController { last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, + enabled: bool } impl Default for LatencyController { @@ -26,6 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), + enabled: true } } } @@ -36,6 +38,11 @@ impl LatencyController { Arc::new(Mutex::new(LatencyController::default())) } + /// Set whether this [LatencyController] should be enabled. + pub fn set_enabled(&mut self, enable: bool) { + self.enabled = enable; + } + /// Update the latency controller with the latest video timestamp. pub fn update_video(&mut self, timestamp: i64) { self.last_video_remote_ts = timestamp; @@ -44,6 +51,10 @@ impl LatencyController { /// Check if the audio should be played based on the current latency. pub fn check_audio(&mut self, timestamp: i64) -> bool { + if !self.enabled { + self.allow_audio = true; + return self.allow_audio; + } // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; let latency = expected - timestamp; diff --git a/src/server/connection.rs b/src/server/connection.rs index 2ee3bc8ed..1924cfca0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -171,6 +171,8 @@ impl Connection { let tx_cloned = tx.clone(); // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); + // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. + latency_controller.lock().unwrap().set_enabled(false); let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { @@ -1561,7 +1563,7 @@ impl Connection { _ => {} }, Some(message::Union::AudioFrame(frame)) => { - if !self.disable_audio { + if !self.disable_audio { allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); } } From e7e8e1a18b6e3bcdf190931869527489704296a4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 22:23:18 +0800 Subject: [PATCH 347/734] opt: send audio frame when connected --- src/client/io_loop.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bac1e5d23..f16c9af72 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -90,7 +90,6 @@ impl Remote { pub async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); - let stop_client_audio = self.start_client_audio(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -114,6 +113,8 @@ impl Remote { 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); + // Start client audio when connection is established. + let stop_client_audio = self.start_client_audio(); // just build for now #[cfg(not(windows))] @@ -218,6 +219,10 @@ impl Remote { } } log::debug!("Exit io_loop of id={}", self.handler.id); + // Stop client audio server. + if let Some(stop) = stop_client_audio { + stop.send(()).ok(); + } } Err(err) => { self.handler @@ -227,9 +232,6 @@ impl Remote { if let Some(stop) = stop_clipboard { stop.send(()).ok(); } - if let Some(stop) = stop_client_audio { - 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); From 4f3c5b42ae158a52a1d276964b38034241e0b187 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 01:39:42 +0800 Subject: [PATCH 348/734] opt: send audio format and data after login successfully. --- src/client/io_loop.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f16c9af72..d568feb4e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -44,6 +44,8 @@ pub struct Remote { audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, + // Stop sending local audio to remote client. + stop_local_audio_sender: Option>, old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, @@ -85,6 +87,7 @@ impl Remote { data_count: Arc::new(AtomicUsize::new(0)), frame_count, video_format: CodecFormat::Unknown, + stop_local_audio_sender: None, } } @@ -113,8 +116,6 @@ impl Remote { 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); - // Start client audio when connection is established. - let stop_client_audio = self.start_client_audio(); // just build for now #[cfg(not(windows))] @@ -220,8 +221,8 @@ impl Remote { } log::debug!("Exit io_loop of id={}", self.handler.id); // Stop client audio server. - if let Some(stop) = stop_client_audio { - stop.send(()).ok(); + if let Some(s) = self.stop_local_audio_sender.take() { + s.send(()).ok(); } } Err(err) => { @@ -865,6 +866,15 @@ impl Remote { }); } } + // Start audio thread for playback + if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } if self.handler.is_file_transfer() { self.handler.load_last_jobs(); From 3b34e2ea453fe6a7667e980fcd05b5d009cc065c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:15:47 +0800 Subject: [PATCH 349/734] feat: run local audio server at start --- flutter/lib/models/native_model.dart | 8 ++++++-- src/ui.rs | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 628bf502d..34a673953 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -118,8 +118,12 @@ class PlatformFFI { // Start a dbus service, no need to await _ffiBind.mainStartDbusServer(); } else if (Platform.isMacOS && isMain) { - // Start an ipc server for handling url schemes. - _ffiBind.mainStartIpcUrlServer(); + Future.wait([ + // Start dbus service. + _ffiBind.mainStartDbusServer(), + // Start local audio pulseaudio server. + _ffiBind.mainStartPa() + ]); } _startListenEvent(_ffiBind); // global event try { diff --git a/src/ui.rs b/src/ui.rs index 8763194fe..7973a0ba4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -95,6 +95,9 @@ pub fn start(args: &mut [String]) { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "index.html"; + // Start pulse audio local server. + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); } else if args[0] == "--install" { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); From 9134c2826e0205aaff80cffe299c0f5c6a71ecc0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:32:46 +0800 Subject: [PATCH 350/734] feat: set audio only mode --- src/client/helper.rs | 19 ++++++++++--------- src/server/connection.rs | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index 005b2df72..248cf5928 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -18,7 +18,7 @@ pub struct LatencyController { last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, - enabled: bool + audio_only: bool } impl Default for LatencyController { @@ -27,7 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), - enabled: true + audio_only: true } } } @@ -38,9 +38,9 @@ impl LatencyController { Arc::new(Mutex::new(LatencyController::default())) } - /// Set whether this [LatencyController] should be enabled. - pub fn set_enabled(&mut self, enable: bool) { - self.enabled = enable; + /// Set whether this [LatencyController] should be working in audio only mode. + pub fn set_audio_only(&mut self, only: bool) { + self.audio_only = only; } /// Update the latency controller with the latest video timestamp. @@ -51,10 +51,6 @@ impl LatencyController { /// Check if the audio should be played based on the current latency. pub fn check_audio(&mut self, timestamp: i64) -> bool { - if !self.enabled { - self.allow_audio = true; - return self.allow_audio; - } // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; let latency = expected - timestamp; @@ -70,6 +66,11 @@ impl LatencyController { self.allow_audio = true; } } + // No video frame here, which means the update time is not triggered. + // We manually update the time here. + if self.audio_only { + self.update_time = Instant::now(); + } self.allow_audio } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1924cfca0..d5c2103b1 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -172,7 +172,7 @@ impl Connection { // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. - latency_controller.lock().unwrap().set_enabled(false); + latency_controller.lock().unwrap().set_audio_only(true); let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { From 95d06e160b21df29a62926fefd46c9f318c2cae1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:51:03 +0800 Subject: [PATCH 351/734] fix: latency --- src/client/helper.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index 248cf5928..e3acf3a44 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -27,7 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), - audio_only: true + audio_only: false } } } @@ -53,7 +53,11 @@ impl LatencyController { pub fn check_audio(&mut self, timestamp: i64) -> bool { // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; - let latency = expected - timestamp; + let latency = if self.audio_only { + expected + } else { + expected - timestamp + }; // Set MAX and MIN, avoid fixing too frequently. if self.allow_audio { if latency.abs() > MAX_LATENCY { @@ -66,11 +70,9 @@ impl LatencyController { self.allow_audio = true; } } - // No video frame here, which means the update time is not triggered. + // No video frame here, which means the update time is not up to date. // We manually update the time here. - if self.audio_only { - self.update_time = Instant::now(); - } + self.update_time = Instant::now(); self.allow_audio } } From cb228bef2b7093115909686a31d304d47eaa6e1e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 20:30:35 +0800 Subject: [PATCH 352/734] feat: add audio switch ui --- flutter/lib/consts.dart | 6 +++ .../lib/desktop/widgets/remote_menubar.dart | 26 +++++++++++++ libs/hbb_common/protos/message.proto | 6 +++ libs/hbb_common/src/config.rs | 10 +++++ src/client.rs | 39 +++++++++++++++++++ src/flutter_ffi.rs | 14 +++++++ 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/eo.rs | 3 ++ src/lang/es.rs | 4 ++ 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 | 5 +++ 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_session_interface.rs | 19 +++++++++ 39 files changed, 219 insertions(+) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index c95c62fcc..99130f892 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -106,6 +106,12 @@ const kRemoteImageQualityLow = 'low'; /// [kRemoteImageQualityCustom] Custom image quality. const kRemoteImageQualityCustom = 'custom'; +/// [kRemoteAudioGuestToHost] Guest to host audio mode(default). +const kRemoteAudioGuestToHost = 'guest-to-host'; + +/// [kRemoteAudioTwoWay] two-way audio mode(default). +const kRemoteAudioTwoWay = 'two-way'; + const kIgnoreDpi = true; /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 36b9504c0..1e5723b61 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1106,6 +1106,30 @@ class _RemoteMenubarState extends State { padding: padding, ), MenuEntryDivider(), + MenuEntryRadios( + text: translate('Audio Transmission Mode'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Guest to Host'), + value: kRemoteAudioGuestToHost, + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Two way'), + value: kRemoteAudioTwoWay, + dismissOnClicked: true, + ), + ], + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetAudioMode(id: widget.id) ?? '', + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetAudioMode(id: widget.id, value: newValue); + } + }, + padding: padding, + ), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { @@ -1337,6 +1361,8 @@ 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 && diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index b7965f237..da4865069 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -444,6 +444,11 @@ enum ImageQuality { Best = 4; } +enum AudioMode { + GuestToHost = 0; + TwoWay = 1; +} + message VideoCodecState { enum PreferCodec { Auto = 0; @@ -475,6 +480,7 @@ message OptionMessage { BoolOption enable_file_transfer = 9; VideoCodecState video_codec_state = 10; int32 custom_fps = 11; + AudioMode audio_mode = 12; } message TestDelay { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 71dd9a5c6..6032ae9c7 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -212,6 +212,11 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, + #[serde( + default = "PeerConfig::default_audio_mode", + deserialize_with = "PeerConfig::deserialize_audio_mode" + )] + pub audio_mode: String, #[serde( default = "PeerConfig::default_custom_image_quality", deserialize_with = "PeerConfig::deserialize_custom_image_quality" @@ -996,6 +1001,11 @@ impl PeerConfig { deserialize_image_quality, UserDefaultConfig::load().get("image_quality") ); + serde_field_string!( + default_audio_mode, + deserialize_audio_mode, + "guest-to-host".to_owned() + ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::load() diff --git a/src/client.rs b/src/client.rs index b2cd0f2f7..54796a935 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1252,6 +1252,27 @@ impl LoginConfigHandler { } } + /// Parse the audio mode option. + /// Return [`AudioMode`] if the option is valid, otherwise return `None`. + /// + /// # Arguments + /// + /// * `q` - The audio mode option. + /// * `ignore_default` - Ignore the default value. + fn get_audio_mode_enum(&self, q: &str, ignore_default: bool) -> Option { + if q == "guest-to-host" { + Some(AudioMode::GuestToHost) + } else if q == "two-way" { + Some(AudioMode::TwoWay) + } else { + if ignore_default { + None + } else { + Some(AudioMode::GuestToHost) + } + } + } + /// Get the status of a toggle option. /// /// # Arguments @@ -1338,6 +1359,24 @@ impl LoginConfigHandler { res } + pub fn save_audio_mode(&mut self, value: String) -> Option { + let mut res = None; + if let Some(q) = self.get_audio_mode_enum(&value, false) { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + audio_mode: q.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + res = Some(msg_out); + } + let mut config = self.load_config(); + config.audio_mode = value; + self.save_config(config); + res + } + /// Create a [`Message`] for saving custom fps. /// /// # Arguments diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d9f67e566..10fd67fdd 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -233,6 +233,20 @@ pub fn session_set_image_quality(id: String, value: String) { } } +pub fn session_get_audio_mode(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_audio_mode()) + } else { + None + } +} + +pub fn session_set_audio_mode(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_audio_mode(value); + } +} + pub fn session_get_keyboard_mode(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_keyboard_mode()) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index f2210f971..197435158 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 00d62946f..c74f352ce 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), + ("Guest to Host", "被控到主机"), + ("Two way", "双向"), + ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 453ecefb3..d956ddf53 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index dcaeb3eaa..9e771567a 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -436,6 +436,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 2d6d3d069..a112385a6 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "fps"), ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0c7f13d7e..342eac51c 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 5fdb7ee2c..74acd8c69 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -445,5 +445,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), + ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index dd1c75bac..50e883227 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3b7f23ab9..9bfdb6b1e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index bc25ab6c6..a569b750f 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 49ce8f140..e28294de0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 0fa6e0293..ece6c9233 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index d84b56a8a..e252219c1 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 35e20d7fd..036bc8ec7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d03b07992..6da983849 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 2006c67d1..459139f58 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b7ccbdbb1..483879d49 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 64e5e9315..cff00333a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0f64ae67f..9fe5eab8c 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7e209dff8..36e2a99de 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 54b064c18..31f24a5e8 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -445,5 +445,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), + ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a703c0799..8cf858df0 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 16c948ceb..0e2208c3c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 285a51732..44159fb4a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index dd943e0e6..892b3664e 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 3050ff635..619a68508 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 7572da9de..f0458b115 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 535e4e772..f61ba325a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 80b384c6c..cade148a7 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index f5d9539d8..46cc90c1e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 37a7d6bcd..7c355edd5 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index d78f5aa7b..f7640ae50 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4fc5db743..234c9a4d7 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -89,6 +89,18 @@ impl Session { self.lc.write().unwrap().save_keyboard_mode(value); } + pub fn get_audio_mode(&self) -> String { + self.lc.read().unwrap().audio_mode.clone() + } + + pub fn save_audio_mode(&self, value: String) { + let msg = self.lc.write().unwrap().save_audio_mode(value); + // Notify remote guest that the audio mode has been changed. + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + pub fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } @@ -653,6 +665,13 @@ impl Session { } } } + + fn get_audio_transmission_mode(&self, id: &str) { + + } + fn set_audio_transmission_mode(&self, id: &str, mode: String) { + + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From 393e0e9afbc69df74f95ee98306fc322c5ef456f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 21:53:26 +0800 Subject: [PATCH 353/734] add: divider --- flutter/lib/desktop/widgets/remote_menubar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 1e5723b61..bb2079930 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1130,6 +1130,7 @@ class _RemoteMenubarState extends State { }, padding: padding, ), + MenuEntryDivider(), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { From 8ab49d11d149de458d6ea95d1543b9c384568632 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 22:06:52 +0800 Subject: [PATCH 354/734] feat: add audio mode config --- src/client.rs | 5 ++-- src/client/io_loop.rs | 46 +++++++++++++++++++++++++++++-------- src/ui_session_interface.rs | 4 ++++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/client.rs b/src/client.rs index 54796a935..d76f930c8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1259,7 +1259,7 @@ impl LoginConfigHandler { /// /// * `q` - The audio mode option. /// * `ignore_default` - Ignore the default value. - fn get_audio_mode_enum(&self, q: &str, ignore_default: bool) -> Option { + pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { if q == "guest-to-host" { Some(AudioMode::GuestToHost) } else if q == "two-way" { @@ -1361,7 +1361,7 @@ impl LoginConfigHandler { pub fn save_audio_mode(&mut self, value: String) -> Option { let mut res = None; - if let Some(q) = self.get_audio_mode_enum(&value, false) { + if let Some(q) = LoginConfigHandler::get_audio_mode_enum(&value, false) { let mut misc = Misc::new(); misc.set_option(OptionMessage { audio_mode: q.into(), @@ -1981,6 +1981,7 @@ pub enum Data { RemovePortForward(i32), AddPortForward((i32, String, i32)), ToggleClipboardFile, + ChangeAudioMode(AudioMode), NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d568feb4e..af8c1048b 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,5 +1,5 @@ use crate::client::{ - Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, + Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -386,6 +386,24 @@ impl Remote { Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } + Data::ChangeAudioMode(audio_mode) => { + match audio_mode { + AudioMode::GuestToHost => { + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + } + AudioMode::TwoWay => { + // Start audio thread for playback. + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } + } + } Data::Message(msg) => { allow_err!(peer.send(&msg).await); } @@ -866,19 +884,27 @@ impl Remote { }); } } - // Start audio thread for playback - if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } if self.handler.is_file_transfer() { self.handler.load_last_jobs(); } + + // Start audio thread for playback if current audio mode is two-way transmission. + if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + let audio_mode = LoginConfigHandler::get_audio_mode_enum( + self.handler.load_config().audio_mode.as_str(), + false, + ) + .unwrap_or(AudioMode::GuestToHost); + if audio_mode == AudioMode::TwoWay { + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } + } } _ => {} }, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 234c9a4d7..73414e405 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -94,6 +94,10 @@ impl Session { } pub fn save_audio_mode(&self, value: String) { + let mode = LoginConfigHandler::get_audio_mode_enum(value.as_str(), false); + if let Some(mode)= mode { + self.send(Data::ChangeAudioMode(mode)); + } let msg = self.lc.write().unwrap().save_audio_mode(value); // Notify remote guest that the audio mode has been changed. if let Some(msg) = msg { From 05822991bfaa48fbf86ff31b734d77a431a75cde Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 22:57:20 +0800 Subject: [PATCH 355/734] opt: rename to dual-way --- flutter/lib/consts.dart | 4 ++-- flutter/lib/desktop/widgets/remote_menubar.dart | 4 ++-- libs/hbb_common/protos/message.proto | 2 +- src/client.rs | 4 ++-- src/client/io_loop.rs | 7 ++++--- 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/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 +- src/ui_session_interface.rs | 7 ------- 38 files changed, 43 insertions(+), 49 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 99130f892..26e25a209 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -109,8 +109,8 @@ const kRemoteImageQualityCustom = 'custom'; /// [kRemoteAudioGuestToHost] Guest to host audio mode(default). const kRemoteAudioGuestToHost = 'guest-to-host'; -/// [kRemoteAudioTwoWay] two-way audio mode(default). -const kRemoteAudioTwoWay = 'two-way'; +/// [kRemoteAudioDualWay] dual-way audio mode(default). +const kRemoteAudioDualWay = 'dual-way'; const kIgnoreDpi = true; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index bb2079930..9864947c6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1115,8 +1115,8 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, ), MenuEntryRadioOption( - text: translate('Two way'), - value: kRemoteAudioTwoWay, + text: translate('Dual way'), + value: kRemoteAudioDualWay, dismissOnClicked: true, ), ], diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index da4865069..48b999438 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -446,7 +446,7 @@ enum ImageQuality { enum AudioMode { GuestToHost = 0; - TwoWay = 1; + DualWay = 1; } message VideoCodecState { diff --git a/src/client.rs b/src/client.rs index d76f930c8..649b180bb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1262,8 +1262,8 @@ impl LoginConfigHandler { pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { if q == "guest-to-host" { Some(AudioMode::GuestToHost) - } else if q == "two-way" { - Some(AudioMode::TwoWay) + } else if q == "dual-way" { + Some(AudioMode::DualWay) } else { if ignore_default { None diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index af8c1048b..a284fdade 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -393,7 +393,7 @@ impl Remote { allow_err!(sender.send(())); } } - AudioMode::TwoWay => { + AudioMode::DualWay => { // Start audio thread for playback. // Cancel previous local audio session. if let Some(sender) = self.stop_local_audio_sender.take() { @@ -889,14 +889,15 @@ impl Remote { self.handler.load_last_jobs(); } - // Start audio thread for playback if current audio mode is two-way transmission. + // Start audio thread for playback if current audio mode is dual-way transmission. if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { let audio_mode = LoginConfigHandler::get_audio_mode_enum( self.handler.load_config().audio_mode.as_str(), false, ) .unwrap_or(AudioMode::GuestToHost); - if audio_mode == AudioMode::TwoWay { + log::debug!("current audio mode: {:?}", audio_mode); + if audio_mode == AudioMode::DualWay { // Cancel previous local audio session. if let Some(sender) = self.stop_local_audio_sender.take() { allow_err!(sender.send(())); diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 197435158..e45dc5fb5 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index c74f352ce..84bfcb384 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "自动"), ("Other Default Options", "其它默认选项"), ("Guest to Host", "被控到主机"), - ("Two way", "双向"), + ("Dual way", "双向"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d956ddf53..ef9cd7bf8 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9e771567a..32aa1f0af 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,7 +437,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index a112385a6..f8fac0737 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 342eac51c..4aa2be8db 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 74acd8c69..932936da3 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -447,7 +447,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Otras opciones predeterminadas"), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 50e883227..b8c45fbee 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9bfdb6b1e..64a8b4e40 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index a569b750f..3918db55b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index e28294de0..edad7ecd0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index ece6c9233..1b2dc4ad5 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index e252219c1..27432303d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 036bc8ec7..ae375b8ee 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 6da983849..417f88fe6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 459139f58..e852278dd 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 483879d49..4cce52e08 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index cff00333a..29252926f 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 9fe5eab8c..8ec40cf13 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 36e2a99de..c4f798abf 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 31f24a5e8..949eba641 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -448,7 +448,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8cf858df0..7de4d10ce 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 0e2208c3c..bf30f96d8 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 44159fb4a..db560166a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 892b3664e..599cd651b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 619a68508..c0616300c 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f0458b115..282b564d7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index f61ba325a..b2bee959a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cade148a7..b6efeaf0a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 46cc90c1e..eea71e6bf 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "自動"), ("Other Default Options", "其它默認選項"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7c355edd5..f0d85a551 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f7640ae50..5e4009570 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 73414e405..1e7848505 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -669,13 +669,6 @@ impl Session { } } } - - fn get_audio_transmission_mode(&self, id: &str) { - - } - fn set_audio_transmission_mode(&self, id: &str, mode: String) { - - } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From cab1fc719aed30a7f0afac289d55e1b03375ac91 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 23:42:38 +0800 Subject: [PATCH 356/734] feat: add audio mode in sciter --- src/ui/header.tis | 10 +++++++++- src/ui/remote.rs | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ui/header.tis b/src/ui/header.tis index dd0b35541..e3f0c70a1 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -183,6 +183,9 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Balanced')}
  • {svg_checkmark}{translate('Optimize reaction time')}
  • {svg_checkmark}{translate('Custom')}
  • +
    +
  • {svg_checkmark}{translate('Guest to Host')}
  • +
  • {svg_checkmark}{translate('Dual way')}
  • {show_codec ?
  • {svg_checkmark}Auto
  • @@ -378,7 +381,7 @@ class Header: Reactor.Component { togglePrivacyMode(me.id); } else if (me.id == "show-quality-monitor") { toggleQualityMonitor(me.id); - }else if (me.attributes.hasClass("toggle-option")) { + } else if (me.attributes.hasClass("toggle-option")) { handler.toggle_option(me.id); toggleMenuState(); } else if (!me.attributes.hasClass("selected")) { @@ -391,6 +394,8 @@ class Header: Reactor.Component { } else if (type == "codec-preference") { handler.set_option("codec-preference", me.id); handler.change_prefer_codec(); + } else if (type == "audio-mode") { + handler.save_audio_mode(me.id); } toggleMenuState(); } @@ -434,6 +439,9 @@ 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); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 21504d20d..541d3a141 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -420,6 +420,8 @@ impl sciter::EventHandler for SciterSession { fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); + fn save_audio_mode(String); + fn get_audio_mode(); } } From 7e5c5b50e5a6dd6b9c1f265cfb1520db4319e739 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 10:01:31 +0800 Subject: [PATCH 357/734] feat: set to default input device when in dual-way --- src/client/io_loop.rs | 10 +++++++-- src/common.rs | 44 +++++++++++++++++++++++++++++++++++++++- src/flutter_ffi.rs | 10 +++++++++ src/platform/linux.rs | 18 ++++++++++++++++ src/server/connection.rs | 7 ++++++- 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index a284fdade..9117c8c5f 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,13 +2,14 @@ use crate::client::{ Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; +use crate::common::{get_default_sound_input, set_sound_input}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; -use hbb_common::futures::channel::mpsc::unbounded; + use hbb_common::tokio::sync::mpsc::error::TryRecvError; use crate::server::Service; @@ -32,7 +33,7 @@ use hbb_common::tokio::{ }; use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; -use std::borrow::Borrow; + use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -270,6 +271,11 @@ impl Remote { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } // Create a channel to receive error or closed message let (tx, rx) = std::sync::mpsc::channel(); let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); diff --git a/src/common.rs b/src/common.rs index c2d5a81f0..9cbc9b150 100644 --- a/src/common.rs +++ b/src/common.rs @@ -30,6 +30,8 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; +use crate::ui_interface::{set_option, get_option}; + pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; pub const CLIPBOARD_NAME: &'static str = "clipboard"; @@ -105,6 +107,46 @@ pub fn check_clipboard( None } +/// Set sound input device. +pub fn set_sound_input(device: String) { + let prior_device = get_option("audio-input".to_owned()); + if prior_device != device { + log::info!("switch to audio input device {}", device); + set_option("audio-input".to_owned(), device); + } else { + log::info!("audio input is already set to {}", device); + } +} + +/// Get system's default sound input device name. +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_default_sound_input() -> Option { + #[cfg(not(target_os = "linux"))] + { + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + let dev = host.default_input_device(); + return if let Some(dev) = dev { + match dev.name() { + Ok(name) => Some(name), + Err(_) => None, + } + } else { + None + }; + } + #[cfg(target_os = "linux")] + { + let input = crate::platform::linux::get_default_pa_source(); + return if let Some(input) = input { + Some(input.1) + } else { + None + }; + } +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { @@ -715,5 +757,5 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin #[cfg(test)] mod test_common { - use super::*; + } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 10fd67fdd..31cb07c07 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -4,6 +4,9 @@ use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; +use crate::common::{is_keyboard_mode_supported, get_default_sound_input}; +use hbb_common::message_proto::KeyboardMode; +use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, @@ -534,6 +537,13 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } +pub fn main_get_default_sound_input() -> Option { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_default_sound_input(); + #[cfg(any(target_os = "android", target_os = "ios"))] + String::from("") +} + pub fn main_get_hostname() -> SyncReturn { SyncReturn(crate::common::hostname()) } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index ac3b32a49..8fa95ac90 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -534,6 +534,24 @@ pub fn get_pa_sources() -> Vec<(String, String)> { out } +pub fn get_default_pa_source() -> Option<(String, String)> { + use pulsectl::controllers::*; + match SourceController::create() { + Ok(mut handler) => { + if let Ok(dev) = handler.get_default_device() { + return Some(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + Err(err) => { + log::error!("Failed to get_pa_source: {:?}", err); + } + } + None +} + pub fn lock_screen() { std::process::Command::new("xdg-screensaver") .arg("lock") diff --git a/src/server/connection.rs b/src/server/connection.rs index d5c2103b1..20cbe0f86 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,7 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}}; +use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}, common::{get_default_sound_input, set_sound_input}}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -1542,6 +1542,11 @@ impl Connection { }, Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); } } From 60925057f0c66ded4d9dc66b52d85b86059ffc1c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 10:23:58 +0800 Subject: [PATCH 358/734] fix: poison error on setting sound input --- src/common.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 9cbc9b150..70d50619e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -112,7 +112,9 @@ pub fn set_sound_input(device: String) { let prior_device = get_option("audio-input".to_owned()); if prior_device != device { log::info!("switch to audio input device {}", device); - set_option("audio-input".to_owned(), device); + std::thread::spawn(move || { + set_option("audio-input".to_owned(), device); + }); } else { log::info!("audio input is already set to {}", device); } From 038d660e6063c6a8222cd7f8c2753ee07492a6e8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 11:10:14 +0800 Subject: [PATCH 359/734] fix: android build --- src/common.rs | 6 ++++++ src/flutter_ffi.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 70d50619e..3e6409c53 100644 --- a/src/common.rs +++ b/src/common.rs @@ -149,6 +149,12 @@ pub fn get_default_sound_input() -> Option { } } +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn get_default_sound_input() -> Option { + None +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 31cb07c07..0fe6818de 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -541,7 +541,7 @@ pub fn main_get_default_sound_input() -> Option { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_default_sound_input(); #[cfg(any(target_os = "android", target_os = "ios"))] - String::from("") + None } pub fn main_get_hostname() -> SyncReturn { From ebec8811c2ec49da7bd3f59db98d38ad0ead84a6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 13:32:10 +0800 Subject: [PATCH 360/734] opt: add microphone permission tip --- flutter/lib/common.dart | 27 ++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 35 +++++++++++++++++-- flutter/macos/Runner/Info.plist | 2 ++ flutter/macos/Runner/MainFlutterWindow.swift | 18 ++++++++++ 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/en.rs | 3 +- 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/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 + 37 files changed, 114 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 30d38b8db..df2a75f56 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1723,3 +1723,30 @@ Future updateSystemWindowTheme() async { } } } +/// macOS only +/// +/// Note: not found a general solution for rust based AVFoundation bingding. +/// [AVFoundation] crate has compile error. +const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos"); + +enum PermissionAuthorizeType { + undetermined, + authorized, + denied, // and restricted +} + +Future osxCanRecordAudio() async { + int res = await kMacOSPermChannel.invokeMethod("canRecordAudio"); + print(res); + if (res > 0) { + return PermissionAuthorizeType.authorized; + } else if (res == 0) { + return PermissionAuthorizeType.undetermined; + } else { + return PermissionAuthorizeType.denied; + } +} + +Future osxRequestAudio() async { + return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0501c298a..71dd2c96e 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -44,6 +44,7 @@ class _DesktopHomePageState extends State var watchIsCanScreenRecording = false; var watchIsProcessTrust = false; var watchIsInputMonitoring = false; + var watchIsCanRecordAudio = false; Timer? _updateTimer; @override @@ -79,7 +80,16 @@ class _DesktopHomePageState extends State buildTip(context), buildIDBoard(context), buildPasswordBoard(context), - buildHelpCards(), + FutureBuilder( + future: buildHelpCards(), + builder: (_, data) { + if (data.hasData) { + return data.data!; + } else { + return const Offstage(); + } + }, + ), ], ), ), @@ -302,7 +312,7 @@ class _DesktopHomePageState extends State ); } - Widget buildHelpCards() { + Future buildHelpCards() async { if (updateUrl.isNotEmpty) { return buildInstallCard( "Status", @@ -348,6 +358,13 @@ class _DesktopHomePageState extends State return buildInstallCard("", "install_daemon_tip", "Install", () async { bind.mainIsInstalledDaemon(prompt: true); }); + } else if ((await osxCanRecordAudio() != + PermissionAuthorizeType.authorized)) { + return buildInstallCard("Permissions", "config_microphone", "Configure", + () async { + osxRequestAudio(); + watchIsCanRecordAudio = true; + }); } } else if (Platform.isLinux) { if (bind.mainCurrentIsWayland()) { @@ -481,6 +498,20 @@ class _DesktopHomePageState extends State setState(() {}); } } + if (watchIsCanRecordAudio) { + if (Platform.isMacOS) { + Future.microtask(() async { + if ((await osxCanRecordAudio() == + PermissionAuthorizeType.authorized)) { + watchIsCanRecordAudio = false; + setState(() {}); + } + }); + } else { + watchIsCanRecordAudio = false; + setState(() {}); + } + } }); Get.put(svcStopped, tag: 'stop-service'); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index c926019ab..96616e8c4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -43,6 +43,8 @@ $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu + NSMicrophoneUsageDescription + Record the sound from microphone for the purpose of the remote desktop. NSPrincipalClass NSApplication diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 97b46bb84..21e870320 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -1,4 +1,5 @@ import Cocoa +import AVFoundation import FlutterMacOS import desktop_multi_window // import bitsdojo_window_macos @@ -81,6 +82,23 @@ class MainFlutterWindow: NSWindow { case "terminate": NSApplication.shared.terminate(self) result(nil) + case "canRecordAudio": + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .authorized: + result(1) + break + case .notDetermined: + result(0) + break + default: + result(-1) + break + } + case "requestRecordAudio": + AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in + result(granted) + }) + break default: result(FlutterMethodNotImplemented) } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e45dc5fb5..e65927876 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 84bfcb384..bcb2c3daf 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), + ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), ("Wait", "等待"), ("Elevation Error", "提权失败"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ef9cd7bf8..d16e3abef 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 32aa1f0af..23884b995 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index f8fac0737..1839edb87 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), + ("config_microphone", ""), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), diff --git a/src/lang/en.rs b/src/lang/en.rs index 6eed43a77..37c08a974 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -41,6 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), ("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.") + ("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.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 4aa2be8db..aa8829874 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 932936da3..da13843f0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -415,6 +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", ""), ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), ("Wait", "Esperar"), ("Elevation Error", "Error de elevación"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b8c45fbee..7664af99e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -415,6 +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 \"Input Monitoring\" را بدهید."), + ("config_microphone", ""), ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), ("Wait", "صبر کنید"), ("Elevation Error", "خطای ارتفاع"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 64a8b4e40..db49b5a78 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), ("Always use software rendering", "Utiliser toujours le rendu logiciel"), ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), + ("config_microphone", ""), ("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."), ("Wait", "En cours"), ("Elevation Error", "Erreur d'augmentation des privilèges"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 3918db55b..5312e6381 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -415,6 +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", ""), ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), ("Wait", "Περιμένετε"), ("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index edad7ecd0..2f6c490ad 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 1b2dc4ad5..7b9325076 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 27432303d..31864b220 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -415,6 +415,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", ""), ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), ("Wait", "Attendi"), ("Elevation Error", "Errore durante l'elevazione dei diritti"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index ae375b8ee..5f2b68c46 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 417f88fe6..59cc9fdff 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e852278dd..8a939764b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 4cce52e08..788aa8b62 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), ("Always use software rendering", "Zawsze używaj renderowania programowego"), ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), + ("config_microphone", ""), ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), ("Wait", "Czekaj"), ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 29252926f..c6899ee54 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8ec40cf13..cdac5f686 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index c4f798abf..5865d0206 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 949eba641..fe1de2e91 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -415,6 +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", ""), ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), ("Wait", "Ждите"), ("Elevation Error", "Ошибка повышения прав"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7de4d10ce..88f09313f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index bf30f96d8..f78a6e9e3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index db560166a..63e834c25 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 599cd651b..33355fd38 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index c0616300c..8af2ccb8a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index 282b564d7..1abc20b36 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index b2bee959a..173143821 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b6efeaf0a..072275334 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index eea71e6bf..8c0968901 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), ("Wait", "等待"), ("Elevation Error", "提權失敗"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index f0d85a551..1934a8eb4 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5e4009570..24c0d9009 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), From 2452a58eaa5e0d14fd5a16e135a40a9acaf547ea Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 14:07:14 +0800 Subject: [PATCH 361/734] opt: rename and move audio transmission mode --- .../lib/desktop/widgets/remote_menubar.dart | 53 ++++++++++--------- src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 4 +- src/lang/de.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 4 +- 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/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 4 +- src/lang/sk.rs | 2 + src/lang/sl.rs | 4 +- 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 + 33 files changed, 91 insertions(+), 34 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 9864947c6..0df962cba 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -884,7 +884,33 @@ class _RemoteMenubarState extends State { // )); // } } - + displayMenu.addAll([ + MenuEntryDivider(), + MenuEntryRadios( + text: translate('Audio Transmission Mode'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Guest to host audio transmission'), + value: kRemoteAudioGuestToHost, + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Dual-way audio transmission'), + value: kRemoteAudioDualWay, + dismissOnClicked: true, + ), + ], + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetAudioMode(id: widget.id) ?? '', + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetAudioMode(id: widget.id, value: newValue); + } + }, + padding: padding, + ), + ]); return displayMenu; } @@ -1106,31 +1132,6 @@ class _RemoteMenubarState extends State { padding: padding, ), MenuEntryDivider(), - MenuEntryRadios( - text: translate('Audio Transmission Mode'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Guest to Host'), - value: kRemoteAudioGuestToHost, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Dual way'), - value: kRemoteAudioDualWay, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetAudioMode(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetAudioMode(id: widget.id, value: newValue); - } - }, - padding: padding, - ), - MenuEntryDivider(), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e65927876..4404e178d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index bcb2c3daf..08f6824c7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "其它默认选项"), ("Guest to Host", "被控到主机"), ("Dual way", "双向"), + ("Guest to host audio transmission", "被控到主机音频传输"), + ("Dual-way audio transmission", "双向音频传输"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d16e3abef..a2a19a37a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 23884b995..905f4814e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,8 +437,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 1839edb87..4028e3337 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Weitere Standardoptionen"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index aa8829874..fe3830b99 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index da13843f0..b9b31f109 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -447,8 +447,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 7664af99e..0b92c6658 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "سایر گزینه های پیش فرض"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index db49b5a78..4965f6dab 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Autres options par défaut"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 5312e6381..e40151ccf 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2f6c490ad..0e1887e48 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7b9325076..689ae98cf 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 31864b220..65f91ecec 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Altre Opzioni Predefinite"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5f2b68c46..33fb2da05 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 59cc9fdff..c874dd695 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 8a939764b..01014bab0 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 788aa8b62..9dd005bdd 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Inne opcje domyślne"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index c6899ee54..716d3df82 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cdac5f686..c7d0cd6ec 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 5865d0206..2d48b91b4 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index fe1de2e91..8224cd5eb 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -448,8 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Другие параметры по умолчанию"), ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 88f09313f..5e0330954 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index f78a6e9e3..a75da46bd 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 63e834c25..d3964a2e9 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 33355fd38..78059645d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 8af2ccb8a..ca2257756 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 1abc20b36..4355d643a 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 173143821..57dfe6e43 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 072275334..49a42af4a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 8c0968901..50e684258 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "其它默認選項"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 1934a8eb4..f37ed341e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 24c0d9009..5788a7f3d 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } From efa4530c97f6eee9c8c8dcd36188218ada8e52f1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 22:49:17 +0800 Subject: [PATCH 362/734] feat: add chat svg --- flutter/assets/chat.svg | 1 + .../lib/desktop/widgets/remote_menubar.dart | 96 +++++++++++-------- src/lang/cn.rs | 2 + 3 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 flutter/assets/chat.svg diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg new file mode 100644 index 000000000..03491be6e --- /dev/null +++ b/flutter/assets/chat.svg @@ -0,0 +1 @@ + \ 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 0df962cba..0004c65fe 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; @@ -478,20 +479,6 @@ class _RemoteMenubarState extends State { ); } - Widget _buildChat(BuildContext context) { - return IconButton( - tooltip: translate('Chat'), - onPressed: () { - widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(); - }, - icon: const Icon( - Icons.message, - color: _MenubarTheme.commonColor, - ), - ); - } - Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( @@ -695,6 +682,60 @@ class _RemoteMenubarState extends State { ); } + Widget _buildChat(BuildContext context) { + FfiModel ffiModel = Provider.of(context); + return mod_menu.PopupMenuButton( + 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, + itemBuilder: (BuildContext context) => _getChatMenu(context) + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + 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: () { + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.toggleChatOverlay(); + }, + padding: padding, + dismissOnClicked: true, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Voice call'), + style: style, + ), + proc: () {}, + padding: padding, + dismissOnClicked: true, + ), + ]); + return chatMenu; + } + List> _getControlMenu(BuildContext context) { final pi = widget.ffi.ffiModel.pi; final perms = widget.ffi.ffiModel.permissions; @@ -884,33 +925,6 @@ class _RemoteMenubarState extends State { // )); // } } - displayMenu.addAll([ - MenuEntryDivider(), - MenuEntryRadios( - text: translate('Audio Transmission Mode'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Guest to host audio transmission'), - value: kRemoteAudioGuestToHost, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Dual-way audio transmission'), - value: kRemoteAudioDualWay, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetAudioMode(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetAudioMode(id: widget.id, value: newValue); - } - }, - padding: padding, - ), - ]); return displayMenu; } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 08f6824c7..65039f0fe 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -450,6 +450,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dual way", "双向"), ("Guest to host audio transmission", "被控到主机音频传输"), ("Dual-way audio transmission", "双向音频传输"), + ("Voice call", "语音通话"), + ("Text chat", "文字聊天"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } From b335d2c82840dd3ef09efc9ebdd7d417ca3a9a25 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 5 Feb 2023 15:36:31 +0800 Subject: [PATCH 363/734] fix: import --- src/client/io_loop.rs | 1 - src/flutter_ffi.rs | 3 --- src/server.rs | 7 ------- 3 files changed, 11 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 9117c8c5f..dcfa7b74e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -12,7 +12,6 @@ use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; use hbb_common::tokio::sync::mpsc::error::TryRecvError; -use crate::server::Service; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::Data, client::Interface}; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0fe6818de..1ecbb0646 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -11,15 +11,12 @@ 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, flutter::{session_add, session_start_}, }; -use crate::common::is_keyboard_mode_supported; use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; diff --git a/src/server.rs b/src/server.rs index bef49f132..616d92375 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,13 +29,6 @@ use service::{GenericService, Service, Subscriber}; use service::ServiceTmpl; use crate::ipc::{connect, Data}; -pub use service::{GenericService, Service, ServiceTmpl, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; pub mod audio_service; cfg_if::cfg_if! { From 45b93100d6d0837d53d12dca30605cc0b10b1ea4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 5 Feb 2023 23:47:06 +0800 Subject: [PATCH 364/734] feat: add voice call proto --- libs/hbb_common/protos/message.proto | 14 ++++++ src/client.rs | 49 +++++++++++---------- src/client/io_loop.rs | 64 ++++++++++++++++++---------- src/flutter_ffi.rs | 18 ++++++-- src/server/connection.rs | 6 +++ src/ui/remote.rs | 8 ++-- src/ui_session_interface.rs | 48 +++++++++++++-------- 7 files changed, 138 insertions(+), 69 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 48b999438..323b464fa 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -604,6 +604,18 @@ message Misc { } } +message VoiceCallRequest { + int64 req_timestamp = 1; + // Indicates whether the request is a connect action or a disconnect action. + bool is_connect = 2; +} + +message VoiceCallResponse { + bool accepted = 1; + int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp]. + int64 ack_timestamp = 3; +} + message Message { oneof union { SignedId signed_id = 3; @@ -626,5 +638,7 @@ message Message { Cliprdr cliprdr = 20; MessageBox message_box = 21; SwitchSidesResponse switch_sides_response = 22; + VoiceCallRequest voice_call_request = 23; + VoiceCallResponse voice_call_response = 24; } } diff --git a/src/client.rs b/src/client.rs index 649b180bb..5911c40ed 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,58 +1,61 @@ -pub use async_trait::async_trait; -use bytes::Bytes; -#[cfg(not(any(target_os = "android", target_os = "linux")))] -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - Device, Host, StreamConfig, -}; -use magnum_opus::{Channels::*, Decoder as AudioDecoder}; -use sha2::{Digest, Sha256}; use std::{ collections::HashMap, net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, + sync::{Arc, atomic::AtomicBool, mpsc, 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}, +}; +use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +use sha2::{Digest, Sha256}; use uuid::Uuid; pub use file_trait::FileManager; use hbb_common::{ + AddrMangle, allow_err, anyhow::{anyhow, Context}, bail, config::{ - Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, + Config, CONNECT_TIMEOUT, PeerConfig, PeerInfoSerde, 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}, - timeout, - tokio::time::Duration, - AddrMangle, ResultType, Stream, + Stream, timeout, tokio::time::Duration, }; -pub use helper::LatencyController; pub use helper::*; +pub use helper::LatencyController; use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, VpxDecoderConfig, VpxVideoCodecId, }; +use crate::{ + common::{self, is_keyboard_mode_supported}, + server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, +}; + pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::{ - common::{self, is_keyboard_mode_supported}, - server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, -}; + 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); @@ -1989,6 +1992,8 @@ pub enum Data { RecordScreen(bool, i32, i32, String), ElevateDirect, ElevateWithLogon(String, String), + NewVoiceCall, + CloseVoiceCall, } /// Keycode for key events. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index dcfa7b74e..67946f546 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,42 +1,38 @@ -use crate::client::{ - Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, - SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, -}; -use crate::common::{get_default_sound_input, set_sound_input}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; -use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicUsize, Ordering}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; - -use hbb_common::tokio::sync::mpsc::error::TryRecvError; - -use crate::ui_session_interface::{InvokeUiSession, Session}; -use crate::{client::Data, client::Interface}; - +use hbb_common::{allow_err, message_proto::*, sleep, get_time}; +use hbb_common::{fs, log, Stream}; use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, + can_enable_overwrite_detection, DigestCheckResult, get_job, get_string, new_send_confirm, RemoveJobMeta, }; use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; -#[cfg(windows)] -use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ self, sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::{allow_err, message_proto::*, sleep}; -use hbb_common::{fs, log, Stream}; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +#[cfg(windows)] +use hbb_common::tokio::sync::Mutex as TokioMutex; -use std::collections::HashMap; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use crate::{audio_service, CLIENT_SERVER, common, ConnInner}; +use crate::{client::Data, client::Interface}; +use crate::client::{ + Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, MILLI1, QualityStatus, SEC30, + SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, +}; +use crate::common::{get_default_sound_input, set_sound_input}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::{check_clipboard, CLIPBOARD_INTERVAL, ClipboardContext, update_clipboard}; +use crate::ui_session_interface::{InvokeUiSession, Session}; pub struct Remote { handler: Session, @@ -752,6 +748,22 @@ impl Remote { msg.set_misc(misc); allow_err!(peer.send(&msg).await); } + Data::NewVoiceCall => { + let mut request = VoiceCallRequest::new(); + request.is_connect = true; + request.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(request); + allow_err!(peer.send(&msg).await); + } + Data::CloseVoiceCall => { + let mut request = VoiceCallRequest::new(); + request.is_connect = false; + request.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(request); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -1262,6 +1274,12 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } + Some(message::Union::VoiceCallRequest(request)) => { + // TODO + } + Some(message::Union::VoiceCallResponse(response)) => { + // TODO + } _ => {} } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1ecbb0646..15bfe90d4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -4,19 +4,19 @@ use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; -use crate::common::{is_keyboard_mode_supported, get_default_sound_input}; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; 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, flutter::{session_add, session_start_}, }; +use crate::common::{get_default_sound_input, is_keyboard_mode_supported}; use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; @@ -840,6 +840,18 @@ pub fn session_new_rdp(id: String) { } } +pub fn session_request_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.request_voice_call(); + } +} + +pub fn session_close_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.close_voice_call(); + } +} + pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } diff --git a/src/server/connection.rs b/src/server/connection.rs index 20cbe0f86..c3acae9cc 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1572,6 +1572,12 @@ impl Connection { allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); } } + Some(message::Union::VoiceCallRequest(request)) => { + // TODO + } + Some(message::Union::VoiceCallResponse(response)) => { + // TODO + } _ => {} } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 541d3a141..1b0d172b9 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -6,12 +6,12 @@ use std::{ use sciter::{ dom::{ - event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, - Element, HELEMENT, + Element, + event::{BEHAVIOR_EVENTS, EVENT_GROUPS, EventReason, PHASE_MASK}, HELEMENT, }, make_args, - video::{video_destination, AssetPtr, COLOR_SPACE}, Value, + video::{AssetPtr, COLOR_SPACE, video_destination}, }; use hbb_common::{ @@ -422,6 +422,8 @@ impl sciter::EventHandler for SciterSession { fn restart_remote_device(); fn save_audio_mode(String); fn get_audio_mode(); + fn request_voice_call(); + fn close_voice_call(); } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 1e7848505..147cd9149 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,26 +1,30 @@ -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}; -use async_trait::async_trait; -use bytes::Bytes; -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 rdev::{Event, EventType::*}; 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::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +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 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::common::{self, GrabState}; +use crate::keyboard; + pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -669,6 +673,14 @@ impl Session { } } } + + pub fn request_voice_call(&self) { + self.send(Data::NewVoiceCall); + } + + pub fn close_voice_call(&self) { + self.send(Data::CloseVoiceCall); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From a04980fa1325a2da1a2625983b1aa016a3153187 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 09:37:52 +0800 Subject: [PATCH 365/734] refactor: remove audio mode --- libs/hbb_common/protos/message.proto | 6 ----- src/client.rs | 40 ---------------------------- src/client/io_loop.rs | 36 ------------------------- src/flutter_ffi.rs | 14 ---------- src/ui/header.tis | 5 ---- src/ui/remote.rs | 2 -- src/ui_session_interface.rs | 16 ----------- 7 files changed, 119 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 323b464fa..ed2706382 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -444,11 +444,6 @@ enum ImageQuality { Best = 4; } -enum AudioMode { - GuestToHost = 0; - DualWay = 1; -} - message VideoCodecState { enum PreferCodec { Auto = 0; @@ -480,7 +475,6 @@ message OptionMessage { BoolOption enable_file_transfer = 9; VideoCodecState video_codec_state = 10; int32 custom_fps = 11; - AudioMode audio_mode = 12; } message TestDelay { diff --git a/src/client.rs b/src/client.rs index 5911c40ed..2ea33b655 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1255,27 +1255,6 @@ impl LoginConfigHandler { } } - /// Parse the audio mode option. - /// Return [`AudioMode`] if the option is valid, otherwise return `None`. - /// - /// # Arguments - /// - /// * `q` - The audio mode option. - /// * `ignore_default` - Ignore the default value. - pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { - if q == "guest-to-host" { - Some(AudioMode::GuestToHost) - } else if q == "dual-way" { - Some(AudioMode::DualWay) - } else { - if ignore_default { - None - } else { - Some(AudioMode::GuestToHost) - } - } - } - /// Get the status of a toggle option. /// /// # Arguments @@ -1362,24 +1341,6 @@ impl LoginConfigHandler { res } - pub fn save_audio_mode(&mut self, value: String) -> Option { - let mut res = None; - if let Some(q) = LoginConfigHandler::get_audio_mode_enum(&value, false) { - let mut misc = Misc::new(); - misc.set_option(OptionMessage { - audio_mode: q.into(), - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - res = Some(msg_out); - } - let mut config = self.load_config(); - config.audio_mode = value; - self.save_config(config); - res - } - /// Create a [`Message`] for saving custom fps. /// /// # Arguments @@ -1984,7 +1945,6 @@ pub enum Data { RemovePortForward(i32), AddPortForward((i32, String, i32)), ToggleClipboardFile, - ChangeAudioMode(AudioMode), NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 67946f546..d0e72a7e6 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -387,24 +387,6 @@ impl Remote { Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } - Data::ChangeAudioMode(audio_mode) => { - match audio_mode { - AudioMode::GuestToHost => { - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - } - AudioMode::DualWay => { - // Start audio thread for playback. - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } - } - } Data::Message(msg) => { allow_err!(peer.send(&msg).await); } @@ -905,24 +887,6 @@ impl Remote { if self.handler.is_file_transfer() { self.handler.load_last_jobs(); } - - // Start audio thread for playback if current audio mode is dual-way transmission. - if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { - let audio_mode = LoginConfigHandler::get_audio_mode_enum( - self.handler.load_config().audio_mode.as_str(), - false, - ) - .unwrap_or(AudioMode::GuestToHost); - log::debug!("current audio mode: {:?}", audio_mode); - if audio_mode == AudioMode::DualWay { - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } - } } _ => {} }, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 15bfe90d4..e28332943 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -233,20 +233,6 @@ pub fn session_set_image_quality(id: String, value: String) { } } -pub fn session_get_audio_mode(id: String) -> Option { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - Some(session.get_audio_mode()) - } else { - None - } -} - -pub fn session_set_audio_mode(id: String, value: String) { - if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - session.save_audio_mode(value); - } -} - pub fn session_get_keyboard_mode(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_keyboard_mode()) diff --git a/src/ui/header.tis b/src/ui/header.tis index e3f0c70a1..009995f4f 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -183,9 +183,6 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Balanced')}
  • {svg_checkmark}{translate('Optimize reaction time')}
  • {svg_checkmark}{translate('Custom')}
  • -
    -
  • {svg_checkmark}{translate('Guest to Host')}
  • -
  • {svg_checkmark}{translate('Dual way')}
  • {show_codec ?
  • {svg_checkmark}Auto
  • @@ -394,8 +391,6 @@ class Header: Reactor.Component { } else if (type == "codec-preference") { handler.set_option("codec-preference", me.id); handler.change_prefer_codec(); - } else if (type == "audio-mode") { - handler.save_audio_mode(me.id); } toggleMenuState(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1b0d172b9..5d6692c3b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -420,8 +420,6 @@ impl sciter::EventHandler for SciterSession { fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); - fn save_audio_mode(String); - fn get_audio_mode(); fn request_voice_call(); fn close_voice_call(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 147cd9149..2f6827523 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -93,22 +93,6 @@ impl Session { self.lc.write().unwrap().save_keyboard_mode(value); } - pub fn get_audio_mode(&self) -> String { - self.lc.read().unwrap().audio_mode.clone() - } - - pub fn save_audio_mode(&self, value: String) { - let mode = LoginConfigHandler::get_audio_mode_enum(value.as_str(), false); - if let Some(mode)= mode { - self.send(Data::ChangeAudioMode(mode)); - } - let msg = self.lc.write().unwrap().save_audio_mode(value); - // Notify remote guest that the audio mode has been changed. - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } - pub fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } From b412a7122b837dd3d9d31c29f04ffc237356d97c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 11:42:25 +0800 Subject: [PATCH 366/734] feat: rust connection implementation --- src/client/helper.rs | 23 +++++- src/client/io_loop.rs | 85 ++++++++++++-------- src/flutter.rs | 16 ++++ src/ipc.rs | 5 +- src/lang/cn.rs | 1 + src/server/connection.rs | 151 ++++++++++++++++++++++++------------ src/ui/remote.rs | 16 ++++ src/ui_session_interface.rs | 4 + 8 files changed, 220 insertions(+), 81 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index e3acf3a44..20acd811a 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -5,7 +5,7 @@ use std::{ use hbb_common::{ log, - message_proto::{video_frame, VideoFrame}, + message_proto::{video_frame, VideoFrame, Message, VoiceCallRequest, VoiceCallResponse}, get_time, }; const MAX_LATENCY: i64 = 500; @@ -115,3 +115,24 @@ pub struct QualityStatus { pub target_bitrate: Option, pub codec_format: Option, } + +#[inline] +pub fn new_voice_call_request(is_connect: bool) -> Message { + let mut req = VoiceCallRequest::new(); + req.is_connect = is_connect; + req.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(req); + msg +} + +#[inline] +pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Message { + let mut resp = VoiceCallResponse::new(); + resp.accepted = accepted; + resp.req_timestamp = request_timestamp; + resp.ack_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_response(resp); + msg +} \ No newline at end of file diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d0e72a7e6..8f2b45321 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,38 +1,40 @@ use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::num::NonZeroI64; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; -use hbb_common::{allow_err, message_proto::*, sleep, get_time}; -use hbb_common::{fs, log, Stream}; use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ - can_enable_overwrite_detection, DigestCheckResult, get_job, get_string, new_send_confirm, + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, }; use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +#[cfg(windows)] +use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ self, sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::tokio::sync::mpsc::error::TryRecvError; -#[cfg(windows)] -use hbb_common::tokio::sync::Mutex as TokioMutex; +use hbb_common::{allow_err, get_time, message_proto::*, sleep}; +use hbb_common::{fs, log, Stream}; -use crate::{audio_service, CLIENT_SERVER, common, ConnInner}; -use crate::{client::Data, client::Interface}; use crate::client::{ - Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, MILLI1, QualityStatus, SEC30, - SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, + new_voice_call_request, Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, + QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, + SERVER_KEYBOARD_ENABLED, }; -use crate::common::{get_default_sound_input, set_sound_input}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, CLIPBOARD_INTERVAL, ClipboardContext, update_clipboard}; +use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +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}; +use crate::{client::Data, client::Interface}; pub struct Remote { handler: Session, @@ -41,7 +43,8 @@ pub struct Remote { receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, // Stop sending local audio to remote client. - stop_local_audio_sender: Option>, + stop_voice_call_sender: Option>, + voice_call_request_timestamp: Option, old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, @@ -83,7 +86,8 @@ impl Remote { data_count: Arc::new(AtomicUsize::new(0)), frame_count, video_format: CodecFormat::Unknown, - stop_local_audio_sender: None, + stop_voice_call_sender: None, + voice_call_request_timestamp: None, } } @@ -217,7 +221,7 @@ impl Remote { } log::debug!("Exit io_loop of id={}", self.handler.id); // Stop client audio server. - if let Some(s) = self.stop_local_audio_sender.take() { + if let Some(s) = self.stop_voice_call_sender.take() { s.send(()).ok(); } } @@ -261,8 +265,15 @@ impl Remote { } } - // Start a local audio recorder, records audio and send to remote - fn start_client_audio(&mut self) -> Option> { + fn stop_voice_call(&mut self) { + let voice_call_sender = std::mem::replace(&mut self.stop_voice_call_sender, None); + if let Some(stopper) = voice_call_sender { + let _ = stopper.send(()); + } + } + + // Start a voice call recorder, records audio and send to remote + fn start_voice_call(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } @@ -731,19 +742,17 @@ impl Remote { allow_err!(peer.send(&msg).await); } Data::NewVoiceCall => { - let mut request = VoiceCallRequest::new(); - request.is_connect = true; - request.req_timestamp = get_time(); - let mut msg = Message::new(); - msg.set_voice_call_request(request); + let msg = new_voice_call_request(true); + // Save the voice call request timestamp for the further validation. + self.voice_call_request_timestamp = Some( + NonZeroI64::new(msg.voice_call_request().req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); allow_err!(peer.send(&msg).await); } Data::CloseVoiceCall => { - let mut request = VoiceCallRequest::new(); - request.is_connect = false; - request.req_timestamp = get_time(); - let mut msg = Message::new(); - msg.set_voice_call_request(request); + self.stop_voice_call(); + let msg = new_voice_call_request(false); allow_err!(peer.send(&msg).await); } _ => {} @@ -1238,11 +1247,25 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } - Some(message::Union::VoiceCallRequest(request)) => { - // TODO + Some(message::Union::VoiceCallRequest(_request)) => { + // TODO: maybe we will do voice call from the peer. } Some(message::Union::VoiceCallResponse(response)) => { - // TODO + let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); + if let Some(ts) = ts { + if response.req_timestamp != ts.get() { + log::debug!("Possible encountering a voice call attack."); + } else { + if response.accepted { + // The peer accepts the voice call. + self.handler.on_voice_call_start(); + self.stop_voice_call_sender = self.start_voice_call(); + } else { + // The peer refused the voice call. + self.handler.on_voice_call_stop("Refused"); + } + } + } } _ => {} } diff --git a/src/flutter.rs b/src/flutter.rs index b4f1f6bc6..7062d85df 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -394,6 +394,22 @@ impl InvokeUiSession for FlutterHandler { fn switch_back(&self, peer_id: &str) { self.push_event("switch_back", [("peer_id", peer_id)].into()); } + + fn on_voice_call_start(&self) { + self.push_event("on_voice_call_start", [].into()); + } + + fn on_voice_call_stop(&self, reason: &str) { + self.push_event("on_voice_call_stop", [("reason", reason)].into()) + } + + fn on_voice_call_waiting(&self) { + self.push_event("on_voice_call_waiting", [].into()); + } + + fn on_voice_call_incoming(&self) { + self.push_event("on_voice_call_incoming", [].into()); + } } /// Create a new remote session with the given id. diff --git a/src/ipc.rs b/src/ipc.rs index d610fb84d..18f618847 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -210,7 +210,10 @@ pub enum Data { DataPortableService(DataPortableService), SwitchSidesRequest(String), SwitchSidesBack, - UrlLink(String) + UrlLink(String), + VoiceCallIncoming, + VoiceCallResponse(bool), + CloseVoiceCall(String), } #[tokio::main(flavor = "current_thread")] diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 65039f0fe..5a9abba9c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -453,5 +453,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "语音通话"), ("Text chat", "文字聊天"), ("Audio Transmission Mode", "音频传输模式"), + ("Refused", "已拒绝") ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index c3acae9cc..1007c71ca 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,11 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}, common::{get_default_sound_input, set_sound_input}}; +use crate::{ + client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + common::{get_default_sound_input, set_sound_input}, + video_service, +}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -32,7 +36,10 @@ use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; -use std::sync::{atomic::AtomicI64, mpsc as std_mpsc}; +use std::{ + num::NonZeroI64, + sync::{atomic::AtomicI64, mpsc as std_mpsc}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; @@ -90,13 +97,19 @@ pub struct Connection { recording: bool, last_test_delay: i64, lock_after_session_end: bool, - show_remote_cursor: bool, // by peer + show_remote_cursor: bool, + // by peer ip: String, - disable_clipboard: bool, // by peer - disable_audio: bool, // by peer - enable_file_transfer: bool, // by peer - audio_sender: MediaSender, // audio by the remote peer/client - tx_input: std_mpsc::Sender, // handle input messages + disable_clipboard: bool, + // by peer + disable_audio: bool, + // by peer + enable_file_transfer: bool, + // by peer + audio_sender: MediaSender, + // audio by the remote peer/client + tx_input: std_mpsc::Sender, + // handle input messages video_ack_required: bool, peer_info: (String, String), server_audit_conn: String, @@ -107,6 +120,8 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + voice_call_request_timestamp: Option, + audio_input_device_before_voice_call: Option, } impl ConnInner { @@ -216,6 +231,8 @@ impl Connection { portable: Default::default(), from_switch: false, audio_sender, + voice_call_request_timestamp: None, + audio_input_device_before_voice_call: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -380,6 +397,12 @@ impl Connection { msg.set_misc(misc); conn.send(msg).await; } + ipc::Data::VoiceCallResponse(accepted) => { + conn.start_voice_call().await; + } + ipc::Data::CloseVoiceCall(_reason) => { + conn.close_voice_call().await; + } _ => {} } }, @@ -650,15 +673,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; @@ -784,7 +807,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(), @@ -811,7 +834,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -855,7 +878,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() { @@ -1138,7 +1161,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1302,12 +1325,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)] @@ -1490,15 +1513,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)] @@ -1508,8 +1531,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(); @@ -1527,8 +1550,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(); @@ -1541,12 +1564,7 @@ impl Connection { _ => {} }, Some(misc::Union::AudioFormat(format)) => { - if !self.disable_audio { - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); - } + if !self.disable_audio { allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); } } @@ -1559,7 +1577,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; @@ -1573,10 +1591,19 @@ impl Connection { } } Some(message::Union::VoiceCallRequest(request)) => { - // TODO + if request.is_connect { + self.voice_call_request_timestamp = Some( + NonZeroI64::new(request.req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + // Call cm. + self.send_to_cm(Data::VoiceCallIncoming); + } else { + self.close_voice_call().await; + } } - Some(message::Union::VoiceCallResponse(response)) => { - // TODO + Some(message::Union::VoiceCallResponse(_response)) => { + // TODO: Maybe we can do a voice call from cm directly. } _ => {} } @@ -1584,6 +1611,34 @@ impl Connection { true } + pub async fn start_voice_call(&self) { + if let Some(ts) = conn.voice_call_request_timestamp.take() { + let msg = new_voice_call_response(ts.get(), accepted); + conn.send(msg).await; + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + conn.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } + } + } else { + log::warn!("Possible a voice call attack."); + } + } + + 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) { + set_sound_input(sound_input); + // Notify the connection manager. + self.send_to_cm(Data::CloseVoiceCall("Closed manually by the peer".to_owned())); + } + } + async fn update_option(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { @@ -1752,13 +1807,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/ui/remote.rs b/src/ui/remote.rs index 5d6692c3b..eb83890d4 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -266,6 +266,22 @@ impl InvokeUiSession for SciterHandler { } fn switch_back(&self, _id: &str) {} + + fn on_voice_call_start(&self) { + self.call("onVoiceCallStart", &make_args!()); + } + + fn on_voice_call_stop(&self, reason: &str) { + self.call("onVoiceCallStop", &make_args!(reason)); + } + + fn on_voice_call_waiting(&self) { + self.call("onVoiceCallWaiting", &make_args!()); + } + + fn on_voice_call_incoming(&self) { + self.call("onVoiceCallIncoming", &make_args!()); + } } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2f6827523..a740b373e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -705,6 +705,10 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); + fn on_voice_call_start(&self); + fn on_voice_call_stop(&self, reason: &str); + fn on_voice_call_waiting(&self); + fn on_voice_call_incoming(&self); } impl Deref for Session { From 11c60088111ba9d9312fd974896afee688a3a722 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 11:53:37 +0800 Subject: [PATCH 367/734] fix: rust conn build --- src/server/connection.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 1007c71ca..87b3f74ea 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -398,7 +398,9 @@ impl Connection { conn.send(msg).await; } ipc::Data::VoiceCallResponse(accepted) => { - conn.start_voice_call().await; + if accepted { + conn.start_voice_call().await; + } } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; @@ -1611,19 +1613,17 @@ impl Connection { true } - pub async fn start_voice_call(&self) { - if let Some(ts) = conn.voice_call_request_timestamp.take() { + pub async fn start_voice_call(&mut self) { + if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); conn.send(msg).await; - if accepted { - // Backup the default input device. - let audio_input_device = Config::get_option("audio-input"); - conn.audio_input_device_before_voice_call = Some(audio_input_device); - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); - } + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + self.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); } } else { log::warn!("Possible a voice call attack."); From a601e3b241eddc3f5a104fee89a8518be79ca34a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:10:15 +0800 Subject: [PATCH 368/734] fix: compile --- src/flutter_ffi.rs | 1 + src/server/connection.rs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e28332943..588733c37 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1288,6 +1288,7 @@ pub fn main_start_ipc_url_server() { /// Send a url scheme throught the ipc. /// /// * macOS only +#[allow(unused_variables)] pub fn send_url_scheme(url: String) { #[cfg(target_os = "macos")] thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); diff --git a/src/server/connection.rs b/src/server/connection.rs index 87b3f74ea..c4c9ec168 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -398,9 +398,7 @@ impl Connection { conn.send(msg).await; } ipc::Data::VoiceCallResponse(accepted) => { - if accepted { - conn.start_voice_call().await; - } + conn.handle_voice_call(accepted).await; } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; @@ -1613,17 +1611,19 @@ impl Connection { true } - pub async fn start_voice_call(&mut self) { + pub async fn handle_voice_call(&mut self, accepted: bool) { if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); - conn.send(msg).await; - // Backup the default input device. - let audio_input_device = Config::get_option("audio-input"); - self.audio_input_device_before_voice_call = Some(audio_input_device); - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); + self.send(msg).await; + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + self.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } } } else { log::warn!("Possible a voice call attack."); From 850c4bcbbf5bfbf152ccda3e876330e5f7286f7e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:14:20 +0800 Subject: [PATCH 369/734] opt: uniform name --- src/client/io_loop.rs | 3 ++- src/flutter.rs | 4 ++-- src/ui/remote.rs | 4 ++-- src/ui_session_interface.rs | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 8f2b45321..e34df30b4 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -749,6 +749,7 @@ impl Remote { .unwrap_or(NonZeroI64::new(get_time()).unwrap()), ); allow_err!(peer.send(&msg).await); + self.handler.on_voice_call_waiting(); } Data::CloseVoiceCall => { self.stop_voice_call(); @@ -1262,7 +1263,7 @@ impl Remote { self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. - self.handler.on_voice_call_stop("Refused"); + self.handler.on_voice_call_closed("Refused"); } } } diff --git a/src/flutter.rs b/src/flutter.rs index 7062d85df..f8d8569ba 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -399,8 +399,8 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_start", [].into()); } - fn on_voice_call_stop(&self, reason: &str) { - self.push_event("on_voice_call_stop", [("reason", reason)].into()) + fn on_voice_call_closed(&self, reason: &str) { + self.push_event("on_voice_call_closed", [("reason", reason)].into()) } fn on_voice_call_waiting(&self) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index eb83890d4..9888e5831 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -271,8 +271,8 @@ impl InvokeUiSession for SciterHandler { self.call("onVoiceCallStart", &make_args!()); } - fn on_voice_call_stop(&self, reason: &str) { - self.call("onVoiceCallStop", &make_args!(reason)); + fn on_voice_call_closed(&self, reason: &str) { + self.call("onVoiceCallClosed", &make_args!(reason)); } fn on_voice_call_waiting(&self) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a740b373e..4b47608f9 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -706,7 +706,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); fn on_voice_call_start(&self); - fn on_voice_call_stop(&self, reason: &str); + fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); } From 040396b3f8421075adce6762010bd74b964d407f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:53:57 +0800 Subject: [PATCH 370/734] feat: cm interface --- src/flutter.rs | 12 ++++++++++++ src/ipc.rs | 1 + src/server/connection.rs | 1 + src/ui/cm.rs | 12 ++++++++++++ src/ui_cm_interface.rs | 27 +++++++++++++++++++++++++++ 5 files changed, 53 insertions(+) diff --git a/src/flutter.rs b/src/flutter.rs index f8d8569ba..e83beb03b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -537,6 +537,18 @@ pub mod connection_manager { fn show_elevation(&self, show: bool) { self.push_event("show_elevation", vec![("show", &show.to_string())]); } + + fn voice_call_started(&self, id: i32) { + self.push_event("voice_call_started", vec![("show", &id.to_string())]); + } + + fn voice_call_incoming(&self, id: i32) { + self.push_event("voice_call_incoming", vec![("id", &id.to_string())]); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.push_event("voice_call_closed", vec![("id", &id.to_string()), ("reason", &reason.to_string())]); + } } impl FlutterHandler { diff --git a/src/ipc.rs b/src/ipc.rs index 18f618847..0ede560fc 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -212,6 +212,7 @@ pub enum Data { SwitchSidesBack, UrlLink(String), VoiceCallIncoming, + StartVoiceCall, VoiceCallResponse(bool), CloseVoiceCall(String), } diff --git a/src/server/connection.rs b/src/server/connection.rs index c4c9ec168..da0126213 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1624,6 +1624,7 @@ impl Connection { if let Some(device) = default_sound_device { set_sound_input(device); } + self.send_to_cm(Data::StartVoiceCall); } } else { log::warn!("Possible a voice call attack."); diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 2bd8824db..dc941c3d0 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -55,6 +55,18 @@ impl InvokeUiCM for SciterHandler { fn show_elevation(&self, show: bool) { self.call("showElevation", &make_args!(show)); } + + fn voice_call_started(&self, id: i32) { + self.call("voice_call_started", &make_args!(id)); + } + + fn voice_call_incoming(&self, id: i32) { + self.call("voice_call_incoming", &make_args!(id)); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.call("voice_call_incoming", &make_args!(id, reason)); + } } impl SciterHandler { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 5d451e4d4..1120a1731 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -88,6 +88,12 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn change_language(&self); fn show_elevation(&self, show: bool); + + fn voice_call_started(&self, id: i32); + + fn voice_call_incoming(&self, id: i32); + + fn voice_call_closed(&self, id: i32, reason: &str); } impl Deref for ConnectionManager { @@ -180,6 +186,18 @@ impl ConnectionManager { fn show_elevation(&self, show: bool) { self.ui_handler.show_elevation(show); } + + fn voice_call_started(&self, id: i32) { + self.ui_handler.voice_call_started(id); + } + + fn voice_call_incoming(&self, id: i32) { + self.ui_handler.voice_call_incoming(id); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.ui_handler.voice_call_closed(id, reason); + } } #[inline] @@ -389,6 +407,15 @@ impl IpcTaskRunner { Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show)) => { self.cm.show_elevation(show); } + Data::StartVoiceCall => { + self.cm.voice_call_started(self.conn_id); + } + Data::VoiceCallIncoming => { + self.cm.voice_call_incoming(self.conn_id); + } + Data::CloseVoiceCall(reason) => { + self.cm.voice_call_closed(self.conn_id, reason.as_str()); + } _ => { } From ea391542fcf607619631c63505df28fd84ec7c67 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 15:36:36 +0800 Subject: [PATCH 371/734] opt: rename to on_voice_call_started --- src/client/io_loop.rs | 2 +- src/flutter.rs | 4 ++-- src/ui/remote.rs | 2 +- src/ui_session_interface.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e34df30b4..d49227864 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1259,7 +1259,7 @@ impl Remote { } else { if response.accepted { // The peer accepts the voice call. - self.handler.on_voice_call_start(); + self.handler.on_voice_call_started(); self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. diff --git a/src/flutter.rs b/src/flutter.rs index e83beb03b..4249e4d94 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -395,8 +395,8 @@ impl InvokeUiSession for FlutterHandler { self.push_event("switch_back", [("peer_id", peer_id)].into()); } - fn on_voice_call_start(&self) { - self.push_event("on_voice_call_start", [].into()); + fn on_voice_call_started(&self) { + self.push_event("on_voice_call_started", [].into()); } fn on_voice_call_closed(&self, reason: &str) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9888e5831..999b409e0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -267,7 +267,7 @@ impl InvokeUiSession for SciterHandler { fn switch_back(&self, _id: &str) {} - fn on_voice_call_start(&self) { + fn on_voice_call_started(&self) { self.call("onVoiceCallStart", &make_args!()); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4b47608f9..f63bbd081 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -705,7 +705,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); - fn on_voice_call_start(&self); + fn on_voice_call_started(&self); fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); From 5e21a81a5cc6aca17ba9a4726a626b14b06a67cc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 20:10:39 +0800 Subject: [PATCH 372/734] wip: implement flutter ui --- Cargo.lock | 5 +-- Cargo.toml | 2 +- flutter/assets/voice_call.svg | 1 + flutter/assets/voice_call_waiting.svg | 1 + .../lib/desktop/widgets/remote_menubar.dart | 32 ++++++++++++++++++- flutter/lib/models/chat_model.dart | 32 +++++++++++++++++++ flutter/lib/models/model.dart | 15 +++++++++ 7 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 flutter/assets/voice_call.svg create mode 100644 flutter/assets/voice_call_waiting.svg diff --git a/Cargo.lock b/Cargo.lock index e15641363..52fcc76cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1334,8 +1334,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" [[package]] name = "default-net" -version = "0.11.0" -source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63" dependencies = [ "libc", "memalloc", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..b315024e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ base64 = "0.13" sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } -default-net = { git = "https://github.com/Kingtous/default-net" } +default-net = "0.12.0" wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg new file mode 100644 index 000000000..0637b58d9 --- /dev/null +++ b/flutter/assets/voice_call.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg new file mode 100644 index 000000000..fd8334f92 --- /dev/null +++ b/flutter/assets/voice_call_waiting.svg @@ -0,0 +1 @@ + \ 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 0004c65fe..d06be52fa 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -426,6 +426,7 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildKeyboard(context)); if (!isWeb) { menubarItems.add(_buildChat(context)); + menubarItems.add(_buildVoiceCall(context)); } menubarItems.add(_buildRecording(context)); menubarItems.add(_buildClose(context)); @@ -707,6 +708,32 @@ class _RemoteMenubarState extends State { ); } + Widget _buildVoiceCall(BuildContext context) { + return Obx( + () { + switch (widget.ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + return SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: _MenubarTheme.commonColor, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ); + break; + case VoiceCallStatus.connected: + return SvgPicture.asset( + "assets/voice_call.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ); + default: + return const Offstage(); + } + }, + ); + } + List> _getChatMenu(BuildContext context) { final List> chatMenu = []; const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); @@ -728,7 +755,10 @@ class _RemoteMenubarState extends State { translate('Voice call'), style: style, ), - proc: () {}, + proc: () { + // Request a voice call. + bind.sessionRequestVoiceCall(id: widget.id); + }, padding: padding, dismissOnClicked: true, ), diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 18a0be279..61602c5b4 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -2,6 +2,7 @@ import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -33,8 +34,13 @@ class ChatModel with ChangeNotifier { OverlayState? _overlayState; OverlayEntry? chatIconOverlayEntry; OverlayEntry? chatWindowOverlayEntry; + bool isConnManager = false; + final Rx _voiceCallStatus = Rx(VoiceCallStatus.notStarted); + + Rx get voiceCallStatus => _voiceCallStatus; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -292,4 +298,30 @@ class ChatModel with ChangeNotifier { resetClientMode() { _messages[clientModeID]?.clear(); } + + void onVoiceCallWaiting() { + _voiceCallStatus.value = VoiceCallStatus.waitingForResponse; + } + + void onVoiceCallStarted() { + _voiceCallStatus.value = VoiceCallStatus.connected; + } + + void onVoiceCallClosed(String reason) { + _voiceCallStatus.value = VoiceCallStatus.notStarted; + } + + void onVoiceCallIncoming() { + if (isConnManager) { + _voiceCallStatus.value = VoiceCallStatus.incoming; + } + } } + +enum VoiceCallStatus { + notStarted, + waitingForResponse, + connected, + // Connection manager only. + incoming +} \ No newline at end of file diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index daf7bfe34..2a4c68839 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -203,6 +203,21 @@ class FfiModel with ChangeNotifier { } else if (name == "on_url_scheme_received") { final url = evt['url'].toString(); parseRustdeskUri(url); + } else if (name == "on_voice_call_waiting") { + // Waiting for the response from the peer. + parent.target?.chatModel.onVoiceCallWaiting(); + } else if (name == "on_voice_call_started") { + // Voice call is connected. + parent.target?.chatModel.onVoiceCallStarted(); + } else if (name == "on_voice_call_closed") { + // Voice call is closed with reason. + final reason = evt['reason'].toString(); + parent.target?.chatModel.onVoiceCallClosed(reason); + } else if (name == "on_voice_call_incoming") { + // Voice call is requested by the peer. + parent.target?.chatModel.onVoiceCallIncoming(); + } else { + debugPrint("Unknown event name: $name"); } }; } From 2943d2d0ccaad9ffe580b98979af95cf44100fb5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:11:55 +0800 Subject: [PATCH 373/734] feat: cm interface --- flutter/lib/desktop/pages/server_page.dart | 42 +++++++++++++++++- .../lib/desktop/widgets/remote_menubar.dart | 32 +++++++++----- flutter/lib/models/chat_model.dart | 4 ++ flutter/lib/models/model.dart | 2 + flutter/lib/models/server_model.dart | 18 ++++++++ src/client/io_loop.rs | 1 + src/flutter.rs | 13 ++---- src/flutter_ffi.rs | 8 ++++ src/server/connection.rs | 2 +- src/ui/cm.rs | 19 ++++---- src/ui_cm_interface.rs | 44 +++++++++++++++---- src/ui_session_interface.rs | 2 +- 12 files changed, 143 insertions(+), 44 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 521413647..b2f70cdd5 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -521,6 +521,38 @@ class _CmControlPanel extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + Offstage( + offstage: !client.inVoiceCall, + child: buildButton(context, + color: Colors.purple, + onClick: () => closeVoiceCall(), + icon: Icon(Icons.reply, color: Colors.white), + text: "Stop voice call", + textColor: Colors.white), + ), + Offstage( + offstage: !client.incomingVoiceCall, + child: Row( + children: [ + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: () => handleVoiceCall(true), + icon: Icon(Icons.phone, color: Colors.white), + text: "Accept", + textColor: Colors.white), + ), + Expanded( + child: buildButton(context, + color: Colors.red, + onClick: () => handleVoiceCall(false), + icon: Icon(Icons.phone, color: Colors.white), + text: "Deny", + textColor: Colors.white), + ) + ], + ), + ), Offstage( offstage: !client.fromSwitch, child: buildButton(context, @@ -626,7 +658,7 @@ class _CmControlPanel extends StatelessWidget { .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); } - buildButton( + Widget buildButton( BuildContext context, { required Color? color, required Function() onClick, @@ -692,6 +724,14 @@ class _CmControlPanel extends StatelessWidget { void handleSwitchBack(BuildContext context) { bind.cmSwitchBack(connId: client.id); } + + void handleVoiceCall(bool accept) { + bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept); + } + + void closeVoiceCall() { + bind.cmCloseVoiceCall(id: client.id); + } } void checkClickTime(int id, Function() callback) async { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d06be52fa..653ff37b1 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -713,19 +713,27 @@ class _RemoteMenubarState extends State { () { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: - return SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - ); - break; + 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 ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + )); case VoiceCallStatus.connected: - return SvgPicture.asset( - "assets/voice_call.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ), ); default: return const Offstage(); diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 61602c5b4..14af96570 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -316,6 +316,10 @@ class ChatModel with ChangeNotifier { _voiceCallStatus.value = VoiceCallStatus.incoming; } } + + void closeVoiceCall(String id) { + bind.sessionCloseVoiceCall(id: id); + } } enum VoiceCallStatus { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2a4c68839..a2fe205af 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -216,6 +216,8 @@ class FfiModel with ChangeNotifier { } else if (name == "on_voice_call_incoming") { // Voice call is requested by the peer. parent.target?.chatModel.onVoiceCallIncoming(); + } else if (name == "update_voice_call_state") { + parent.target?.serverModel.updateVoiceCallState(evt); } else { debugPrint("Unknown event name: $name"); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 56dca4cdf..6cd905c37 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -579,6 +579,20 @@ class ServerModel with ChangeNotifier { notifyListeners(); } } + + void updateVoiceCallState(Map evt) { + try { + final client = Client.fromJson(jsonDecode(evt["client"])); + final index = _clients.indexWhere((element) => element.id == client.id); + if (index != -1) { + _clients[index].inVoiceCall = evt['in_voice_call']; + _clients[index].incomingVoiceCall = evt['incoming_voice_call']; + notifyListeners(); + } + } catch (e) { + debugPrint("updateVoiceCallState failed: $e"); + } + } } enum ClientType { @@ -602,6 +616,8 @@ class Client { bool recording = false; bool disconnected = false; bool fromSwitch = false; + bool inVoiceCall = false; + bool incomingVoiceCall = false; RxBool hasUnreadChatMessage = false.obs; @@ -623,6 +639,8 @@ class Client { recording = json['recording']; disconnected = json['disconnected']; fromSwitch = json['from_switch']; + inVoiceCall = json['in_voice_call']; + incomingVoiceCall = json['incoming_voice_call']; } Map toJson() { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d49227864..aa51df378 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -754,6 +754,7 @@ 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"); allow_err!(peer.send(&msg).await); } _ => {} diff --git a/src/flutter.rs b/src/flutter.rs index 4249e4d94..a27a9d4e1 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -538,16 +538,9 @@ pub mod connection_manager { self.push_event("show_elevation", vec![("show", &show.to_string())]); } - fn voice_call_started(&self, id: i32) { - self.push_event("voice_call_started", vec![("show", &id.to_string())]); - } - - fn voice_call_incoming(&self, id: i32) { - self.push_event("voice_call_incoming", vec![("id", &id.to_string())]); - } - - fn voice_call_closed(&self, id: i32, reason: &str) { - self.push_event("voice_call_closed", vec![("id", &id.to_string()), ("reason", &reason.to_string())]); + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + self.push_event("update_voice_call_state", vec![("client", &client_json)]); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 588733c37..cfca0e082 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -838,6 +838,14 @@ pub fn session_close_voice_call(id: String) { } } +pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { + crate::ui_cm_interface::handle_incoming_voice_call(id, accept); +} + +pub fn cm_close_voice_call(id: i32) { + crate::ui_cm_interface::close_voice_call(id); +} + pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } diff --git a/src/server/connection.rs b/src/server/connection.rs index da0126213..1e88b9b05 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1636,7 +1636,7 @@ impl Connection { 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. - self.send_to_cm(Data::CloseVoiceCall("Closed manually by the peer".to_owned())); + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } } diff --git a/src/ui/cm.rs b/src/ui/cm.rs index dc941c3d0..cce553154 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -56,16 +56,15 @@ impl InvokeUiCM for SciterHandler { self.call("showElevation", &make_args!(show)); } - fn voice_call_started(&self, id: i32) { - self.call("voice_call_started", &make_args!(id)); - } - - fn voice_call_incoming(&self, id: i32) { - self.call("voice_call_incoming", &make_args!(id)); - } - - fn voice_call_closed(&self, id: i32, reason: &str) { - self.call("voice_call_incoming", &make_args!(id, reason)); + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "updateVoiceCallState", + &make_args!( + client.id, + client.in_voice_call, + client.incoming_voice_call + ), + ); } } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 1120a1731..ccddab0ee 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -49,6 +49,8 @@ pub struct Client { pub restart: bool, pub recording: bool, pub from_switch: bool, + pub in_voice_call: bool, + pub incoming_voice_call: bool, #[serde(skip)] tx: UnboundedSender, } @@ -89,11 +91,7 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn show_elevation(&self, show: bool); - fn voice_call_started(&self, id: i32); - - fn voice_call_incoming(&self, id: i32); - - fn voice_call_closed(&self, id: i32, reason: &str); + fn update_voice_call_state(&self, client: &Client); } impl Deref for ConnectionManager { @@ -144,6 +142,8 @@ impl ConnectionManager { recording, from_switch, tx, + in_voice_call: false, + incoming_voice_call: false }; CLIENTS .write() @@ -188,15 +188,27 @@ impl ConnectionManager { } fn voice_call_started(&self, id: i32) { - self.ui_handler.voice_call_started(id); + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = true; + self.ui_handler.update_voice_call_state(client); + } } fn voice_call_incoming(&self, id: i32) { - self.ui_handler.voice_call_incoming(id); + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = true; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } } - fn voice_call_closed(&self, id: i32, reason: &str) { - self.ui_handler.voice_call_closed(id, reason); + fn voice_call_closed(&self, id: i32, _reason: &str) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } } } @@ -832,3 +844,17 @@ pub fn elevate_portable(_id: i32) { } } } + +#[inline] +pub fn handle_incoming_voice_call(id: i32, accept: bool) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::VoiceCallResponse(accept))); + }; +} + +#[inline] +pub fn close_voice_call(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); + }; +} \ No newline at end of file diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f63bbd081..cd0bdcde2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -661,7 +661,7 @@ impl Session { pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } - + pub fn close_voice_call(&self) { self.send(Data::CloseVoiceCall); } From bd07f60a1109aff1ef0aa87b8621b2d80ee326b6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 16:41:33 +0800 Subject: [PATCH 374/734] disable blank issue, use better format --- .github/ISSUE_TEMPLATE/bug_report.md | 32 -------------- .github/ISSUE_TEMPLATE/bug_report.yaml | 49 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 - .github/ISSUE_TEMPLATE/feature_request.md | 10 ----- .github/ISSUE_TEMPLATE/feature_request.yaml | 25 +++++++++++ 5 files changed, 74 insertions(+), 43 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 5ba29c8b6..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug Report -about: Report a bug (English only, Please). -title: "" -labels: bug -assignees: '' - ---- - - - -**Describe the bug you encountered:** - -... - -**What did you expect to happen instead?** - -... - - -**How did you install `RustDesk`?** - - - ---- - -**RustDesk version and environment** - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..87fc6a5f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,49 @@ +name: Bug Report +description: Create a bug report to help us improve +title: "[Bug] " +body: + - type: textarea + id: desc + attributes: + label: Bug Description + description: A clear and concise description of what the bug is + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: How to Reproduce + description: What steps can we take to reproduce this behavior? + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen + validations: + required: true + - type: input + id: os + attributes: + label: Operating System + description: What OS are you seeing this bug on? local side / remote side. + validations: + required: true + - type: input + id: version + attributes: + label: RustDesk Version(s) + description: What version(s) of RustDesk do you see this bug on? local side / remote side. + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, please add screenshots to help explain your problem + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any additonal context about the problem here diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 01de3b330..7b43e397b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,3 @@ -blank_issues_enabled: true contact_links: - name: Ask a question url: https://github.com/rustdesk/rustdesk/discussions/category_choices diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 0d21f017d..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for this project ((English only, Please). -title: '' -labels: enhancement -assignees: '' - ---- - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 000000000..01f6c6aca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,25 @@ +name: Feature Request +description: Suggest an idea for RustDesk +title: "[FR] " +body: + - type: textarea + id: desc + attributes: + label: Description + description: Describe your suggested feature and the main use cases + validations: + required: true + + - type: textarea + id: users + attributes: + label: Impact + description: What types of users can benefit from using the suggested feature? + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any additonal context about the feature here From fc933ad7b4c8e88f035aea44694ff53721895a33 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:47:19 +0800 Subject: [PATCH 375/734] fix: voice call 1 --- flutter/lib/models/server_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 6cd905c37..eec424bfe 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -585,8 +585,8 @@ class ServerModel with ChangeNotifier { final client = Client.fromJson(jsonDecode(evt["client"])); final index = _clients.indexWhere((element) => element.id == client.id); if (index != -1) { - _clients[index].inVoiceCall = evt['in_voice_call']; - _clients[index].incomingVoiceCall = evt['incoming_voice_call']; + _clients[index].inVoiceCall = client.inVoiceCall; + _clients[index].incomingVoiceCall = client.incomingVoiceCall; notifyListeners(); } } catch (e) { From cd6cdbff8f9c9fb38d7ad9631634ba2b9bea328d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:53:46 +0800 Subject: [PATCH 376/734] fix: close notify --- src/client/io_loop.rs | 2 +- src/server/connection.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index aa51df378..234f4f842 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1264,7 +1264,7 @@ impl Remote { self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. - self.handler.on_voice_call_closed("Refused"); + self.handler.on_voice_call_closed(""); } } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1e88b9b05..7a16df811 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1614,7 +1614,6 @@ impl Connection { pub async fn handle_voice_call(&mut self, accepted: bool) { if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); - self.send(msg).await; if accepted { // Backup the default input device. let audio_input_device = Config::get_option("audio-input"); @@ -1625,7 +1624,10 @@ impl Connection { set_sound_input(device); } self.send_to_cm(Data::StartVoiceCall); + } else { + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } + self.send(msg).await; } else { log::warn!("Possible a voice call attack."); } From b82df0913731e60e87be25149c238ba5bb0c3e67 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 17:00:36 +0800 Subject: [PATCH 377/734] new SECURITY.md --- docs/SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/SECURITY.md diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 000000000..c595885f2 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +We value security for the project very highly. We encourage all users to report any vulnerabilities they discover to us. +If you find a security vulnerability in the RustDesk project, please report it responsibly by sending an email to info@rustdesk.com. + +At this juncture, we don't have a bug bounty program. We are a small team trying to solve a big problem. We urge you to report any vulnerabilities responsibly +so that we can continue building a secure application for the entire community. From 66aaf243cf7654c40628187a0249ac77b9452c7a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 17:09:36 +0800 Subject: [PATCH 378/734] opt: notify cm --- flutter/lib/desktop/pages/server_page.dart | 7 ++++--- flutter/lib/models/server_model.dart | 6 ++++++ src/client/io_loop.rs | 2 +- src/server/connection.rs | 7 ++++--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b2f70cdd5..a253b9aa2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -524,7 +524,7 @@ class _CmControlPanel extends StatelessWidget { Offstage( offstage: !client.inVoiceCall, child: buildButton(context, - color: Colors.purple, + color: Colors.red, onClick: () => closeVoiceCall(), icon: Icon(Icons.reply, color: Colors.white), text: "Stop voice call", @@ -538,7 +538,7 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: MyTheme.accent, onClick: () => handleVoiceCall(true), - icon: Icon(Icons.phone, color: Colors.white), + icon: Icon(Icons.phone_enabled, color: Colors.white), text: "Accept", textColor: Colors.white), ), @@ -546,7 +546,8 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: Colors.red, onClick: () => handleVoiceCall(false), - icon: Icon(Icons.phone, color: Colors.white), + icon: + Icon(Icons.phone_disabled_rounded, color: Colors.white), text: "Deny", textColor: Colors.white), ) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index eec424bfe..aab12ab5d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -587,6 +587,12 @@ class ServerModel with ChangeNotifier { if (index != -1) { _clients[index].inVoiceCall = client.inVoiceCall; _clients[index].incomingVoiceCall = client.incomingVoiceCall; + if (client.incomingVoiceCall) { + // Has incoming phone call, let's set the window on top. + Future.delayed(Duration.zero, () { + window_on_top(null); + }); + } notifyListeners(); } } catch (e) { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 234f4f842..05eab692a 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1259,7 +1259,7 @@ impl Remote { log::debug!("Possible encountering a voice call attack."); } else { if response.accepted { - // The peer accepts the voice call. + // The peer accepted the voice call. self.handler.on_voice_call_started(); self.stop_voice_call_sender = self.start_voice_call(); } else { diff --git a/src/server/connection.rs b/src/server/connection.rs index 7a16df811..86d837619 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1596,9 +1596,11 @@ impl Connection { NonZeroI64::new(request.req_timestamp) .unwrap_or(NonZeroI64::new(get_time()).unwrap()), ); - // Call cm. + // Notify the connection manager. self.send_to_cm(Data::VoiceCallIncoming); } else { + // Notify the connection manager. + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.close_voice_call().await; } } @@ -1617,6 +1619,7 @@ impl Connection { if accepted { // Backup the default input device. let audio_input_device = Config::get_option("audio-input"); + log::debug!("Backup the sound input device {}", audio_input_device); self.audio_input_device_before_voice_call = Some(audio_input_device); // Switch to default input device let default_sound_device = get_default_sound_input(); @@ -1637,8 +1640,6 @@ impl Connection { // Restore to the prior audio device. 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. - self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } } From bdbb9ac2887e7af7785c3718f79e86a792058056 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 17:15:01 +0800 Subject: [PATCH 379/734] opt: issues template --- .github/ISSUE_TEMPLATE/bug_report.yaml | 22 ++++++++++++---------- .github/ISSUE_TEMPLATE/task.yaml | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/task.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 87fc6a5f5..d3036ba24 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,7 +1,14 @@ name: Bug Report -description: Create a bug report to help us improve +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: @@ -23,18 +30,13 @@ body: description: A clear and concise description of what you expected to happen validations: required: true - - type: input - id: os - attributes: - label: Operating System - description: What OS are you seeing this bug on? local side / remote side. - validations: - required: true - type: input id: version attributes: - label: RustDesk Version(s) - description: What version(s) of RustDesk do you see this bug on? local side / remote side. + label: Operating System(s) and RustDesk Version(s) on local side and remote side + description: What Operatiing System(s) and version(s) of RustDesk do you see this bug on? local side / remote side. + placeholder: | + Windows 10, 1.1.9 / osx 13.1, 1.1.8 validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml new file mode 100644 index 000000000..a1ff080c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yaml @@ -0,0 +1,20 @@ +name: 📝 Task +description: Create a task for the team to work on +title: "[Task]: " +labels: [Task] +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 + attributes: + label: SubTasks + placeholder: | + - Sub Task 1 + - Sub Task 2 + validations: + required: false From 29b1d106aa8385b03a40ecfa7e125831a3920caf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 17:16:06 +0800 Subject: [PATCH 380/734] opt: ui and message --- flutter/lib/desktop/pages/server_page.dart | 4 ++-- src/lang/ca.rs | 8 +++----- src/lang/cn.rs | 7 +------ src/lang/cs.rs | 8 +++----- src/lang/da.rs | 6 +++--- src/lang/de.rs | 8 +++----- src/lang/eo.rs | 8 +++----- src/lang/es.rs | 9 ++++----- src/lang/fa.rs | 8 +++----- src/lang/fr.rs | 8 +++----- src/lang/gr.rs | 8 +++----- src/lang/hu.rs | 8 +++----- src/lang/id.rs | 8 +++----- src/lang/it.rs | 8 +++----- src/lang/ja.rs | 8 +++----- src/lang/ko.rs | 8 +++----- src/lang/kz.rs | 8 +++----- src/lang/pl.rs | 8 +++----- src/lang/pt_PT.rs | 8 +++----- src/lang/ptbr.rs | 8 +++----- src/lang/ro.rs | 8 +++----- src/lang/ru.rs | 12 +++++------- src/lang/sk.rs | 8 +++----- src/lang/sl.rs | 6 +++--- src/lang/sq.rs | 8 +++----- src/lang/sr.rs | 8 +++----- src/lang/sv.rs | 8 +++----- src/lang/template.rs | 8 +++----- src/lang/th.rs | 8 +++----- src/lang/tr.rs | 8 +++----- src/lang/tw.rs | 8 +++----- src/lang/ua.rs | 8 +++----- src/lang/vn.rs | 8 +++----- 33 files changed, 99 insertions(+), 161 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index a253b9aa2..66a043fef 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -526,7 +526,7 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: Colors.red, onClick: () => closeVoiceCall(), - icon: Icon(Icons.reply, color: Colors.white), + icon: Icon(Icons.phone_disabled_rounded, color: Colors.white), text: "Stop voice call", textColor: Colors.white), ), @@ -548,7 +548,7 @@ class _CmControlPanel extends StatelessWidget { onClick: () => handleVoiceCall(false), icon: Icon(Icons.phone_disabled_rounded, color: Colors.white), - text: "Deny", + text: "Dismiss", textColor: Colors.white), ) ], diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 4404e178d..e98c6636a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5a9abba9c..64c37709a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -446,13 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), - ("Guest to Host", "被控到主机"), - ("Dual way", "双向"), - ("Guest to host audio transmission", "被控到主机音频传输"), - ("Dual-way audio transmission", "双向音频传输"), ("Voice call", "语音通话"), ("Text chat", "文字聊天"), - ("Audio Transmission Mode", "音频传输模式"), - ("Refused", "已拒绝") + ("Stop voice call", "停止语音聊天"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a2a19a37a..70a3eb6c7 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 905f4814e..ae943e1e8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,9 +437,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), @@ -449,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 4028e3337..44bbafdac 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "fps"), ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index fe3830b99..f457833f8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index b9b31f109..220447454 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -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", "Cerrado como se esperaba"), + ("Closed as expected", ""), ("Display", "Pantalla"), ("Default View Style", "Estilo de vista predeterminado"), ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), @@ -446,9 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), - ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 0b92c6658..c206f91ff 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4965f6dab..39ee3bc7f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index e40151ccf..7cb678ecc 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 0e1887e48..25562f556 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 689ae98cf..68a80e540 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 65f91ecec..9730bbc2d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 33fb2da05..7069c0daf 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index c874dd695..43eb552d3 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 01014bab0..49c7b9916 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9dd005bdd..41239961a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 716d3df82..e69a140c9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index c7d0cd6ec..0887a5915 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 2d48b91b4..304353d42 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8224cd5eb..1e6c6962a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -435,8 +435,8 @@ 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", "Стиль прокрутки по умолчанию"), @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), - ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 5e0330954..6f6f7a18e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index a75da46bd..2fb74fa5d 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index d3964a2e9..5d4a6e1ad 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 78059645d..31a3ade8f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ca2257756..e30c09e44 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 4355d643a..b88618074 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 57dfe6e43..1c75aaae7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 49a42af4a..a9e2c1715 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 50e684258..7c49a29a2 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index f37ed341e..92c99d90c 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5788a7f3d..8bb1d45e9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } From 926afc908fb00fc626a9a3012777bd73a3a5c1b2 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:20:53 +0800 Subject: [PATCH 381/734] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index d3036ba24..9bf1f6153 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -2,13 +2,13 @@ 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: 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 9d0e4bdad0d81d2827d1dd0f506df2285e566791 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:22:23 +0800 Subject: [PATCH 382/734] Update feature_request.yaml --- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 01f6c6aca..ab4e9ae39 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,4 +1,4 @@ -name: Feature Request +name: 🛠️ Feature request description: Suggest an idea for RustDesk title: "[FR] " body: From f9864c1d0f77a3a9824d53934715eeca6bb4fb47 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:23:09 +0800 Subject: [PATCH 383/734] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 9bf1f6153..16509a3be 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,4 +1,4 @@ -name: Bug Report +name: 🐞 Bug report description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** title: "[Bug] " body: From 79ca1aa116e2d53c24bfa67f402c18e1cb4ea827 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:30:53 +0800 Subject: [PATCH 384/734] Update feature_request.yaml --- .github/ISSUE_TEMPLATE/feature_request.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index ab4e9ae39..50cd6d0cf 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -2,6 +2,14 @@ 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 4ea41b52d3066031f8ea8ac32942c7e67f36eada Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 18:01:54 +0800 Subject: [PATCH 385/734] fix: execution order of listening ipc thread --- flutter/lib/main.dart | 3 +++ src/client.rs | 2 -- src/client/io_loop.rs | 2 +- src/core_main.rs | 2 -- src/flutter_ffi.rs | 4 ++++ 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c19adf753..b923a31e1 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -114,6 +114,9 @@ Future initEnv(String appType) async { _registerEventHandler(); // Update the system theme. updateSystemWindowTheme(); + if (appType == kAppTypeConnectionManager) { + await bind.cmStartListenIpcThread(); + } } void runMainApp(bool startService) async { diff --git a/src/client.rs b/src/client.rs index 2ea33b655..020bea1f0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -773,7 +773,6 @@ impl AudioHandler { unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; self.simple.as_mut().map(|x| x.write(data_u8)); } - log::debug!("write Audio frame {} to system.", frame.timestamp); } }); } @@ -1595,7 +1594,6 @@ pub fn start_audio_thread( if let Ok(data) = audio_receiver.recv() { match data { MediaData::AudioFrame(af) => { - log::debug!("recved audio frame={}", af.timestamp); audio_handler.handle_frame(af); } MediaData::AudioFormat(f) => { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 05eab692a..c8a0f2ca3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -300,7 +300,7 @@ impl Remote { // check if client is closed match rx.try_recv() { Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit local audio service of client"); + log::debug!("Exit voice call audio service of client"); // unsubscribe CLIENT_SERVER.write().unwrap().subscribe( audio_service::NAME, diff --git a/src/core_main.rs b/src/core_main.rs index 99d0e888e..03d057eff 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -246,8 +246,6 @@ pub fn core_main() -> Option> { } else if args[0] == "--cm" { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel - #[cfg(feature = "flutter")] - crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index cfca0e082..84407cd96 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1284,6 +1284,10 @@ pub fn main_hide_docker() -> SyncReturn { SyncReturn(true) } +pub fn cm_start_listen_ipc_thread() { + crate::flutter::connection_manager::start_listen_ipc_thread(); +} + /// Start an ipc server for receiving the url scheme. /// /// * Should only be called in the main flutter window. From 795b0068d0deefa1eeb99a52c8b6cef1fd1e30d5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 18:17:31 +0800 Subject: [PATCH 386/734] opt: close voice call msg --- src/server/connection.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 86d837619..1bacad124 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1599,8 +1599,6 @@ impl Connection { // Notify the connection manager. self.send_to_cm(Data::VoiceCallIncoming); } else { - // Notify the connection manager. - self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.close_voice_call().await; } } @@ -1641,6 +1639,7 @@ impl Connection { if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); } + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } async fn update_option(&mut self, o: &OptionMessage) { From db8b6d618f0d6b93b69f97dcfc42bca26063b2cf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:09:22 +0800 Subject: [PATCH 387/734] fix: audio close status sync --- src/client/io_loop.rs | 11 +++++++++-- src/server/connection.rs | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index c8a0f2ca3..96ddd51f0 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1249,8 +1249,15 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } - Some(message::Union::VoiceCallRequest(_request)) => { - // TODO: maybe we will do voice call from the peer. + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + // TODO: maybe we will do voice call from the peer in the future. + } else { + if let Some(sender) = self.stop_voice_call_sender.take() { + allow_err!(sender.send(())); + self.handler.on_voice_call_closed(""); + } + } } Some(message::Union::VoiceCallResponse(response)) => { let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); diff --git a/src/server/connection.rs b/src/server/connection.rs index 1bacad124..17417cf61 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -402,6 +402,9 @@ impl Connection { } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; + // Notify the peer that we closed the voice call. + let req = new_voice_call_request(false); + conn.send(req).await; } _ => {} } @@ -1639,6 +1642,7 @@ impl Connection { 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. self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } From c4b1c51e9e745f32037e04c3ae17fd4a6f0799a5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:33:58 +0800 Subject: [PATCH 388/734] opt: more debug info --- flutter/lib/main.dart | 4 +--- src/flutter.rs | 4 +++- src/server/connection.rs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b923a31e1..c61287d4f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -114,9 +114,6 @@ Future initEnv(String appType) async { _registerEventHandler(); // Update the system theme. updateSystemWindowTheme(); - if (appType == kAppTypeConnectionManager) { - await bind.cmStartListenIpcThread(); - } } void runMainApp(bool startService) async { @@ -219,6 +216,7 @@ void runMultiWindow( void runConnectionManagerScreen(bool hide) async { await initEnv(kAppTypeConnectionManager); + await bind.cmStartListenIpcThread(); _runApp( '', const DesktopServerPage(), diff --git a/src/flutter.rs b/src/flutter.rs index a27a9d4e1..2d7d3fb86 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -549,9 +549,11 @@ 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); }; } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 17417cf61..a8849b4e6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -401,10 +401,11 @@ impl Connection { conn.handle_voice_call(accepted).await; } ipc::Data::CloseVoiceCall(_reason) => { + log::debug!("Close the voice call from the ipc."); conn.close_voice_call().await; // Notify the peer that we closed the voice call. - let req = new_voice_call_request(false); - conn.send(req).await; + let msg = new_voice_call_request(false); + conn.send(msg).await; } _ => {} } From 86b88c2927a0251dcd8cdbd90e799ced45bb5d04 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:40:50 +0800 Subject: [PATCH 389/734] opt: open audio when needed --- src/client/io_loop.rs | 3 ++- src/server/connection.rs | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 96ddd51f0..f5792bce3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1251,8 +1251,9 @@ impl Remote { } Some(message::Union::VoiceCallRequest(request)) => { if request.is_connect { - // TODO: maybe we will do voice call from the peer in the future. + // TODO: maybe we will do a voice call from the peer in the future. } else { + log::debug!("The remote has requested to close the voice call"); if let Some(sender) = self.stop_voice_call_sender.take() { allow_err!(sender.send(())); self.handler.on_voice_call_closed(""); diff --git a/src/server/connection.rs b/src/server/connection.rs index a8849b4e6..02888d1ea 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -106,7 +106,7 @@ pub struct Connection { // by peer enable_file_transfer: bool, // by peer - audio_sender: MediaSender, + audio_sender: Option, // audio by the remote peer/client tx_input: std_mpsc::Sender, // handle input messages @@ -184,11 +184,6 @@ impl Connection { let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); - // Start a audio thread to play the audio sent by peer. - let latency_controller = LatencyController::new(); - // 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); - let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { id, @@ -230,7 +225,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, - audio_sender, + audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, }; @@ -1569,7 +1564,14 @@ impl Connection { }, Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { - allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); + // Drop the audio sender previously. + std::mem::replace(&mut self.audio_sender, None); + // Start a audio thread to play the audio sent by peer. + let latency_controller = LatencyController::new(); + // 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.unwrap().send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] From 404915c97512cbb9a60d58f70ae9eb83c60c2733 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:49:42 +0800 Subject: [PATCH 390/734] fix: compile --- src/server/connection.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 02888d1ea..9ce53c960 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1565,13 +1565,13 @@ impl Connection { Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { // Drop the audio sender previously. - std::mem::replace(&mut self.audio_sender, None); + drop(std::mem::replace(&mut self.audio_sender, None)); // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); // 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.unwrap().send(MediaData::AudioFormat(format))); + allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] @@ -1593,7 +1593,11 @@ impl Connection { }, Some(message::Union::AudioFrame(frame)) => { if !self.disable_audio { - allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); + 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."); + } } } Some(message::Union::VoiceCallRequest(request)) => { From 344d927ff8bbd090b02967ba0e1217cbdb1776f2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:38:27 +0800 Subject: [PATCH 391/734] opt: optimize icon --- flutter/assets/record_screen.svg | 24 +++++ flutter/assets/voice_call.svg | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 16 ++-- .../lib/desktop/widgets/remote_menubar.dart | 93 ++++++++++++------- flutter/lib/models/chat_model.dart | 2 +- 5 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 flutter/assets/record_screen.svg diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg new file mode 100644 index 000000000..e1b962124 --- /dev/null +++ b/flutter/assets/record_screen.svg @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg index 0637b58d9..5654befc7 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/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 71dd2c96e..2986adc7a 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -358,14 +358,16 @@ class _DesktopHomePageState extends State return buildInstallCard("", "install_daemon_tip", "Install", () async { bind.mainIsInstalledDaemon(prompt: true); }); - } else if ((await osxCanRecordAudio() != - PermissionAuthorizeType.authorized)) { - return buildInstallCard("Permissions", "config_microphone", "Configure", - () async { - osxRequestAudio(); - watchIsCanRecordAudio = true; - }); } + //// Disable microphone configuration for macOS. We will request the permission when needed. + // else if ((await osxCanRecordAudio() != + // PermissionAuthorizeType.authorized)) { + // return buildInstallCard("Permissions", "config_microphone", "Configure", + // () async { + // osxRequestAudio(); + // watchIsCanRecordAudio = true; + // }); + // } } else if (Platform.isLinux) { if (bind.mainCurrentIsWayland()) { return buildInstallCard( diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 653ff37b1..dcc531408 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -657,12 +657,17 @@ class _RemoteMenubarState extends State { ? translate('Stop session recording') : translate('Start session recording'), onPressed: () => value.toggle(), - icon: Icon( - value.start - ? Icons.pause_circle_filled - : Icons.videocam_outlined, - color: _MenubarTheme.commonColor, - ), + 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, + ), )); } else { return Offstage(); @@ -708,36 +713,58 @@ 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, + width: Theme.of(context).iconTheme.size ?? 20.0, + height: Theme.of(context).iconTheme.size ?? 20.0, + )); + case VoiceCallStatus.connected: + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: Icon( + Icons.phone_disabled_rounded, + color: Colors.red, + size: Theme.of(context).iconTheme.size ?? 22.0, + ), + ); + 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( () { - 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 ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - )); - case VoiceCallStatus.connected: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - ), - ); - default: - return const Offstage(); - } + final tooltipText = _getVoiceCallTooltip(); + return tooltipText == null + ? const Offstage() + : IconButton( + padding: EdgeInsets.zero, + icon: _getVoiceCallIcon(), + tooltip: translate(tooltipText), + onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + ); }, ); } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 14af96570..bf7f8773d 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -328,4 +328,4 @@ enum VoiceCallStatus { connected, // Connection manager only. incoming -} \ No newline at end of file +} From c3b273a5add1f208a50c062f69906f45fc680156 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:48:09 +0800 Subject: [PATCH 392/734] fix: android compile --- src/flutter_ffi.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 84407cd96..2e6c450c1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1285,6 +1285,7 @@ pub fn main_hide_docker() -> SyncReturn { } 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 e944b776bc6ce9afe31ac3ce1b7e6f4520cd8f18 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:59:13 +0800 Subject: [PATCH 393/734] opt: remove unnecessary config field --- libs/hbb_common/src/config.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 6032ae9c7..71dd9a5c6 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -212,11 +212,6 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, - #[serde( - default = "PeerConfig::default_audio_mode", - deserialize_with = "PeerConfig::deserialize_audio_mode" - )] - pub audio_mode: String, #[serde( default = "PeerConfig::default_custom_image_quality", deserialize_with = "PeerConfig::deserialize_custom_image_quality" @@ -1001,11 +996,6 @@ impl PeerConfig { deserialize_image_quality, UserDefaultConfig::load().get("image_quality") ); - serde_field_string!( - default_audio_mode, - deserialize_audio_mode, - "guest-to-host".to_owned() - ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::load() From 3ca72e82a0b26d8a1aa38bbb731f9c7119b66953 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 21:04:50 +0800 Subject: [PATCH 394/734] new logo design --- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 1605 -> 3114 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1087 -> 1939 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 2097 -> 4087 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 3013 -> 6636 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 3799 -> 8908 bytes .../Icon-App-1024x1024@1x.png | Bin 10508 -> 49903 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 360 -> 669 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 564 -> 1344 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 779 -> 2049 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 455 -> 969 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 781 -> 1948 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1072 -> 3139 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 564 -> 1344 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 978 -> 2846 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 1368 -> 4240 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 1368 -> 4240 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1962 -> 6893 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 926 -> 2594 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1691 -> 5794 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1839 -> 6468 bytes .../macos/Runner.xcodeproj/project.pbxproj | 9 +- .../AppIcon.appiconset/Contents.json | 130 +++++++++--------- .../AppIcon.appiconset/app_icon_1024.png | Bin 23562 -> 53345 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 2409 -> 5475 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 338 -> 978 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 4616 -> 10828 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 644 -> 1555 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 9733 -> 23370 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 1222 -> 2851 bytes flutter/pubspec.lock | 8 ++ flutter/pubspec.yaml | 29 ++-- flutter/web/icons/Icon-192.png | Bin 4103 -> 8908 bytes flutter/web/icons/Icon-512.png | Bin 12570 -> 25973 bytes flutter/web/icons/Icon-maskable-192.png | Bin 4106 -> 8908 bytes flutter/web/icons/Icon-maskable-512.png | Bin 12626 -> 25973 bytes flutter/web/manifest.json | 2 +- flutter/windows/runner/resources/app_icon.ico | Bin 21592 -> 1961 bytes res/128x128.png | Bin 1575 -> 75 bytes res/128x128@2x.png | Bin 2760 -> 10623 bytes res/32x32.png | Bin 493 -> 74 bytes res/64x64.png | Bin 2264 -> 74 bytes res/icon-margin.png | Bin 12179 -> 0 bytes res/icon.ico | Bin 34072 -> 48 bytes res/icon.png | Bin 12963 -> 60426 bytes res/logo-header.svg | 2 +- res/mac-icon.png | Bin 90116 -> 51695 bytes res/mac-tray-dark-x2.png | Bin 809 -> 1585 bytes res/mac-tray-dark.png | Bin 275 -> 535 bytes res/mac-tray-light-x2.png | Bin 810 -> 1193 bytes res/mac-tray-light.png | Bin 270 -> 415 bytes res/tray-icon.ico | Bin 4286 -> 4286 bytes 51 files changed, 96 insertions(+), 84 deletions(-) mode change 100644 => 120000 res/128x128.png mode change 100644 => 120000 res/32x32.png mode change 100644 => 120000 res/64x64.png delete mode 100644 res/icon-margin.png mode change 100644 => 120000 res/icon.ico 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 d5d2c49c89429f17812a634e49dc5407b4782bb1..eac2fe7241381b7d162fb15323837ea7101e4a84 100644 GIT binary patch delta 3110 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 A(EtDd delta 1589 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 diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 41ccba607c6c0a5c84d367341ca562d2e8b5e264..d32c8f8e80bec54065a4cfdbac465fb35af8124d 100644 GIT binary patch literal 4087 zcmV=4-7XGR`olZI{kc35qgjEp`2ndP`=#hXh;*2t)jCxScC}&Vdk7q<@oKf7)pdLkW z6wc@mS5{$ET-XGZEhZ3wps1LHC14gpNIL1g=DuD?b$505tLm*HeCJ$9)vNA$b-!Ep z-Fx4w>J(8F!2yBfYLXjCE+ZL8(v2jA=fmd!93)L7Ka+e*@;S){5@)0jMp9q~$^9gA zN%}UvMh5dNXGsVoU=hwMKc1LQ z@&?Hq-wr7OjG6OpC;qsHWE(tg=_~;(+IcxyJvs;R3dt8Fbv^=^<)4ps5{E7%kC8mY z2w(&6+~oF09(I?Aea#M(ownm(s{@D44ji#KQEzjh+2MkPl1P?D8udU{ih#US9WF@M zqrXXyi!%)vk!8S`90M}+x@2F99A=X|&In)`t|pQ!7b$e_X$L+(X~V`kD@sZ6?M~5S zB0hm+8=n8Ily6#ztOlC{KM+ZqB*&$Y?nh)Ba7`B@rsk#L%3LE1IzheziA4^{WJbV^ z$?i!dS@}EF7JPWrf}`y&_$gVSf28FS={83j-6$w)wxjHf9WT|ipbOn4zt2y{>|Pld zpOZ?5C6$axjDWGpI{Vf-I~E);V>A8lZ{1-O8@R2ot%=;%+qGuAeZq|4#wW*Zo*mJ+7$wJsZ6g;*1N?23jaYc(43=Kh6{E9^F}Yq2 zbY%p@y`;q#JzPW8{GqgO@>%pdL*R8 zUHP?bF3kSMjP|QJWS#BX?^7AnftR4PsAr&2?yCy_i8YOA-`(~@Yex-;h0c@~+V7mpcS6bym%dpA| zX!B!4=Gc&J2qu%01fyQSN5k__oSmxl(p4j-1Uyk{#p?%b5&U%`+>!Dqi*%1=!@6Ng zURrcYP>7fj@cMomp4(&V*b$W*L5$oHxb6gI`O7Ztikk}3Ra|*=hzS8}ezD{JuPvSp zKkpF1jl|s@MIJAVI$-U{u9%#kSj+bzY65CnD9+zV11^g+-F{!@qE%UN{M`|AJIhCO zNt*H*q9VXaS?yixEoi1OH=+geTDy!JNg*ljmVz-P!*cZ~%G4vDOgEDn*x_vy>(*Oc zIMU|C_bm>THrcVe$y>^ne2?@TX=M4!Ms>mT?#WX=LsSGj`>h>Yk4qMR#gGI&ZBOcD z!0kmwOzUkxZmP0L6$>eUOM?v`9JOF|Z9Cdr9p_-gts310B?ZcBJ{2uCymGV!HC88v zWTfDc3$sxaMc$zLla-AwJiXiI&KyI#92F!GZY@m3Gb7V6AX~4tdq^YyGP%3aEsJbf zSW?}Fou};RPj}C}A`|*$D8KYC8!fo$y9W169P93uCJR>7wV|}QhiVd3eTtp`;jK0} znDNW`eZOsf62|7I;MH*^T$Uf({E$t;%pa5))pvYS-;Npk8r?H*(j2G3=E9sG8nJOi zfvU+JRag2&wFB#`oeEq|coI@5`+TxE4fBT^q1VQAp&vf3Bjta03O3eXnP6aZlNGBN zK?Nqo->sMccJ8Nk+WEuB2qfgDQucYh2@`uIr2MNG>*_3+QE|#!M)&TNA>pa2X55^g zu6SlmF##KDoY-9}21FGI?P9`^kVm1&`l%ThnWx^@#}jK$w&T`{rjV8|-L5n{u#U!v z-{mPw<`fgKV252snE|mj2MNsbH<0p2XjJ(hk@9EmY4Tr58-auukF>;>fQnyT*nUh> z+gF;+dbgR+UsGbztn&YN(t?@gr(t(nK7y0J5F~7)v0#6z4Z|||i+PF&c;|pKIP44} z0TJENf-y!+xImi^*X%!I$E{^e9_35HNy3ujW-J~Y2Lf2uyX>HoBdQ7SjxO(^$KOUK zuH35>|EX$oFZ5EoVBozIX1q8c*PVy(fSZ8Q6CzHuLSo^{lPvbp*RD>}WT@%Gv374! zOXZ|aGu=9yDM7n|b1dK{;LF3V5Q8tjJGyOf3W^J~>GypYoom3SC#?Zdo-z`?Xtc(j zfQ?nIFfJ#uJ7Q}zpB|@O*W<&zh3RBgr+FQ#3V+Bh2Q~O1Yo)D<^?flBX7F+xJUxU4ANZ4LY2* z`*I!y?#Rp5cP)&7$a4)`1RSjQo}4QJ3T_+IU5Bhx?M6O}7f&MPc={)Yohdw6nm=%%)gmp&^ z()F-mz(v3rHt8PX1Y=3Glym{=`G z?z%YLk(JK_ac06HBOpE!I-mV}bsQ$KjJT|Z^P;&3NF{5qIzuTsG6`*oTW%jSum?2& z4n>D(?u24o?Cb#4k?RlW4I)oyA-GQn{QmGnVD(JQH=O)q4^ZBpf-z zHy~*VRc4nmmlM#|&A=CWa}m%xTfo6O9&49x`@VsNWSyrJ!BE-k9Ys}=(8I`gy<7wg z=qg~n`*u`ELh13&cL(gOw?_y=!?zWs$B=*_1yJbs{o8gN6Jeu4JeAWgCL1h<-`(I) z9)?QV268Tah=c?06OI(h^}LgNd8?bV^IJ(z?P9YsKY)T3m8Hbc<8 z4Nly-#p`-}_QB=%gWHDX=y7G9vdIs{A<1Lc0@L0K=W;s!X3wpyyiJd(gSD&tDW92P zmQFZ@3?c%zgWK*OXj1%~m|_BMplE1ZUlF@%!jr&WIQ8)zI$YOZn{H>-VLR^ns?9T3 zAp|J4L#yB=;X;c2?<`XO*sEd!SYQ8jJurp6V?Bi5H!z<+OPk7P%j_4FS@Cqa%}>p; zie=!u6CS-J-Tlr9rBIa>l?)azvA>9~4kL(!j8p-u?+{QjNPEiPdBTYYzP6yO-dpGq z!YP{H9SzRbV@`i{Pu!~_fc5*$TLfISSoE8y$)Y}dGF!kkDvECVe|2H;w=UE)ix|~Y zz}(S#T$mj_iEMB!{nd$uWbtc$ad>sf>vAHKprat;t;;i2SwDm7y1<}(VBaGGUfL?+ zyIP=so`Anz#r7-j$lC8+n7PVv_Q@3XEW!&rotQRAhr5RBFuA|ZU5`?NCX0wQKRfW| zemlM;a@|Vw`w9h_ZWt2&JTML83)C;>Q-T%D=-B_e@H8UbsV<*#scSiY=+B9}PSPLrz5bba5mXsik2yGO6Sq!6%T zzld2YJa0DlK%w#yMFQl%=PyH2G56xwyxutB1gzLc%3tpCDBtHQ)F>|zB*?gZmK}ygz3G;@f;+667dB8kj1XQ0w1VzV^1jK+KPpJ9o)kfSm zG=9%FCyaoL3*gR}6GKAucS+wYit|g-aYfJg&f+DEfWKcSVBJ9xEo{zP!z3^-bKfun zp1Z<`9AoTVP9PWo4kUnxp*?|J4+^+vxrkjyph*%g>MG#vNk&{AXRDXM!4%JaY&bs= zXB*NT*gj9ddwWDYvk~~IK^r9W$p+?+HQ?@{DKU?7BA}TOzH zV$A^-9SPWdBsnBpS|H&5Vgvp(M4xEI`%uLQD23-|xDyH%(%mu~ZYe(oEZGHoSRta- zy)H@x35ay8=kGKFaMK_?=8e+hir#u8MeQBf%LrgUK>uJedr{n5z|vcQmv00%(f#+ zhwwGY9O;fg0@y0a86@YAohx9L&y?RL-6>ZCvU?BQO0o!^AD}-E^h*6s(}Ci002ovPDHLkV1jJkn{WUC delta 2085 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 qM%IAf*eABYz8wNklBs*w#M07ZwsQ7(k@#I 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 N002ovPDHLkV1m#B3G)B| diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index c35862a8cad79e203ff3a6a98b58612aa478d041..16cef3177fb50c285857dd57f2fb57647a41df88 100644 GIT binary patch literal 49903 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 literal 10508 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 delta 344 zcmV-e0jK_*1?U2h8Gi!+006pI?LPnj0De$RR7C)B|NsC05PJXm{QsuS|8%ndEsFnl zv;Q)U|L*qx%i{l@$^U(~|8TJXV5k2+l>ZWY{|9yd1atrO`Tz0v|J>~V$Ke0L-T#KW z|0jq4A%p)LfdA?9|LF4n;_v^p)&H>5|E|*imc;*$!2f2d|9@Mg|4^R)N16XQk^dQf z|4o3B(f|MerAb6VR2Ufrz=e^+FaQL=J;@Zd9fW6s{u|7weE|SUTlD|{v%wqOP5@1^1=A_{1*UzLfL-zd49NvB*PMY} z(gPf_1iEAe7*Uf~U|wWVfK9Rnd?xP`><+0n!1$6J`?+gsvIfvSCmEU~D*)V^nr@Zj q7Epll=f49KEdT%;wJiWZ+0+N!F$M@7z2^)70000iq_u zlkrGE_C^p1QjQLmh`dGM{x7hMao6oghdbCk5&mmV!Vm)v0|=DA99U`Mx8&+yEXb~Q`Z9ZRYQiA9XA%BNA>>#y;6n1-Mc1s0C4I!+D zBv!23S+IogtDT`xB5r z)J8JGAAj~r07E8(HWPR81!F}Hd(b4-KIl6<=)0SkAG>_@HWRNoGzIQ-A*UzmSWWr} z0_b=yJ?iPPz~LJ{0M$qh`=w4#ur4nvG5Ci8q#ifU-uswPQ<{C?iI{g)HW9vZYucU1F7oTG z`rfIq@=u(7Yfkn=P_B++!6VZ^sa@q6)z!5RrRU_kR&5n}Jz-T_ln4S>K+4tWS+F;V z6@8!;s@S}w9=FhJ2A&Nwgx;_^W@%!qd@0DNBg7ekl}Qe(TXe;r>@L;Se^@qJXBZc8^vQjTx3bl z;WuaHE0dBUAQ~aE8@YVFS|C&%KT`&WCVv&H;+#Rd zI{mT~?W!7fv6*|`Mkyc#7C3ltE7?-h9N!jJ<4s zt=|B?y$Ub7aQhCfc?3-yz#(SRNinCeg4|~zktKnnUBXTT8v>1@i)>tVruYa&$ za_Fu`5SD>~o)O!+<<<)_`TEV>Qnejc+n~*&)XUf`cHEJFpOE8YAv?G595}JjB=V_7 zAE3w;OKt5!ZQb)jF3EGpD0a9}e7MK+-F}gY^V9X>=0e>(X_f0TyF3RS4@PFAR!+|x1#db~sHt9i43A{YQ-WBE-7$m&ZROWEU zw~L$Y$z&%5n2gRnuj4QK;3qx!=F|c0@?k*3bX`}8Gi!+006rnNM8T|0F6*gR7C)B|NsC02X+7a{r^0Y|4N$wAcOy9 zs{dc6|N8v@M3?{Y_y6nj|J&>Tz1sh!&Hq1?|2B{RE{p#ffBz17|MU3&jgS!8IxBqak|9?=P{|a{h#NPk2)c=jY z|BS!?RG|Mlk^k4||DekMqd?oG0004ENkl7qjx+eexTz(c?%J*R;cr; zZScx;n5!UVZg<98%aMB5qa!{U#7+O6`m)?)0LAE3cTq7wbkyz< o1Dh_O-XEAC?es_Z$YwS250E_%Xl{s52><{907*qoM6N<$f=It2u>b%7 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index ab315a4c6b0e580ee9e2cc9567ac156af384c918..18ebaab699c080d107c6faffc1c9ddf0819dec5e 100644 GIT binary patch delta 2046 zcmVU2gpf8K~#7F?V4+B6jc<*@15EAwq4q8X-k(r zSJC*wR3HLsHxWpve$o#{e7`lm1&tDouS89#e9**z8iTJd`oUNPiqdL8u|<@qhyvS| zwm{pZ-M;oQJCEa?-EDVg_D*+anPQBdY^K|py?cLi=bUrT|9{>!0RZ~9ndzSf$jAPN zK3DdihbA!{h)F$RaX2Q0L=xcOV6of6)Ywf`Rt95iR7VCtADa|fPjl^OVqHN#z)J`N zB6x`-lX#C@W_*Rq+*D$1F0(BzFrVcOA_3|9|+R0@FQJ&U-2g=9!su2OX2(oukp${s<3oQj#?Zil!+k+%qTtni4h~ z9e<@~;_>Rj^$QCNO}f!_L+>5p@B1;bCYJU%H@8@N&#n}*1@{kKRi*yHN zPS9fE5K;kW5H}%iZ*ujcPmz7rQALH!M?fpW_&wMKEX60VyHr?*EuB|fHI9b*A3`ND=!?8~X zqHV(*ncKe0Wpk?~8l0vE4JYmi3%;KMC!%7!7Qp_nu(^M7W35XEbclyH zx_AT$m!NaoSoGN{$8|OOCNrw_hJ~iCGksAhnV{l+xT$aQVNaopHp$bWKRhf3LMZJk z)PLNpOl&uV_+&)9wsWK}BBs?aa;IZFynib6BrS{6q2E0u#Gik92r9T=U1~Lim=j^u zkE8x6ak`9Abh`EA)DsH}GV53h^q(WrFN2_*s}9H9h-413V(N1 zA>%2`Q`l))sfDSvvpo?Z{vU;#T*?A1r|0x!r{wm!3?@z(@oX}jst}yv9wtguX=>=H z*lcBfsckNpku1w4L`~J?WJpvmk!C~{AdS=^pTzp5Q7#5@>8f3StC=&Pon}>g1$Qtq z7tAOagOpEZa62+eLN#=m6KAwfaesFQGw#|5^G}M3=vQ*PE$Tp0L)Xkx3TY)b$t{5} zlsSxalG71sAOwnPNy(ilBGvy_7NTMs4n{R(m4tWurFEAWnW}f3jLA{Rr2Ap2Ukig9 zGNh0inv_&aJ-F={(vnr5{BAJxHa`~+eLDAL#U=}rLzUN5kK2w?WL1K@?SHuB4?;t+ zF&?siSlBx_Q{_>(eQruQNrArpLiFqog{(^FmL%L`5Hu<7o;na#hM=1Jx(aJf&=-~C zODo{lK|1&OWoTu!F~j`kZ`}SNF(U+1+#UzB+)a@h^pfq~W%!XV)H0S>Lka%+Hr8e` z0D4zH|LlRN{KXVkPM*BGj(=Xb)1mLU0=>8!kAxN6^(DCDE_R{I0OFl}{0(1+1raHi zffU?!Gk(x(r#7cU7g+JrP4LWDDOV<}-`vTZJ+TPTawzs(Cr5y%YXUkqAuhM+T=#f% zC3>$D9XlhpuXf>%dvRR}g_sjyUq95iu$OjKW&2Kv>%Wh7Cj2<1lz%DQZX2`lV)`)8 z25mFrjtAkbeex50bOl;K8{>g-SpAW3WCSn+s~eb`FJ{)%Gvx*9PxehnJA1`#NBI4N ziEX6JxmL}+xzSl-)hvT&(@s?`{CczI)$UPPwN2~}%3}tSoWI-rGLy%JYv-YPc3F%B zIp`jj2E%DPUE1qaGk@H-G?>@b&!+bC6nw`=!1um5FcI%jS)Y*MkDrH`{Wz@^0xEZ7 z3A1InbM`&Ee4vk?0pEL4l31f2ls~6hyv%{Su83(}UEs);QguW3je&0q3{0kpUoCF! zGKk7uU&QWdc2+vFD>~iKE#IMmF{Ip#Gl09%&9<&}R629gpntldCABn4&6n}ZRBog~ zRB_*Ofq9G1menirhkoK}^yOjVB-*a5XXmBNjyGRpy?K#I*KWCP==xH8cmr(u7H!^( zC%F8%?Pm1QV)MpjW}-RtGUSsz$Rz*b8uZvz@NOsG+6Dd*ecTI+n7c1!A6RTEcjj3X zt<807*qoM6N<$f*ObL!2kdN delta 766 zcmVjjH|7olLTBH9vlK(M`{~m(>@%R70-2c7W z|Fzcte765?uK#AL|4*I&NSXiI>i^d0|Hjm&N~!zW+s+|KsoffVlr$rT?ta|EJFXIa2;}0006ZNklT6oujcIAvyTA2Z|rmo|!A`=v^pY5j>6ofxhOrP}MR;b^8dyif{B$s;G* zwru!(N`zipLVwAWD7{aiMJwJ;i!3Pkm^nhr|EB&6}9TXz0`ai zosjQcHI@1T&Xh_94US^&o3h@3nXK6ZN2p)O=>QcuFH`eCtA9g)ajWEmoPn%!0w&t4mn`im#qQlAA+Ih!=<@lRm(54ZP~@ZZ=kKLo&8_yj z6zFQNG9m9u*-aI$v|9P4zmoG9s)dt0aiD)Er%k|{yt^qLqZ@f+0{YMLs*6pZRCkbx z>Ybp;OpY7+y|U(puw7Ld1E8gh+XiJGv*|ZL8F%MB%I#+II)~v!Bl=lUU2%+cQt&cZ zb~#ReeeTbjOG#>WCa1gsyAQvYgFaAO%-^^DP_#z0Izyseb3wEP+7jtWsMRA%<5@`U wsC45}Lel3hqR-EgQ0j76GaSv-uNO-F1G>T-l-XG?PXGV_07*qoM6N<$f|yI5N&o-= diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 6d69c01e18397d3da443238836eb42b69cacc901..a8ee14a31e4b0fe52fed14f5e6cd05e0b625d7f8 100644 GIT binary patch delta 958 zcmV;v13~=91IY)F8Gix*007zX@K^u<1CU8XK~#7FwU*mY6G0Tl&+Kk%cgv+M2?bi@ zVnli8Z|*pL}R=>h(2k2@`4gIMj=5XRI~^v zrGVXDU}wf@yKQ&NN->6$KJ0d8e>>-#Z@yW9QVR1B!hAsI%6}b=k^ZnEV=4)#QskWr z`2XU5npCdzM>~e2U&0FT6u88XhYBm_Ew(hdtSj=&|Hci*$jNu%HeZyWBnxQ<>41z| z2jmO>(2+8!ZK(qxG?QS!?Fr(BC!w!l3><>LX3W}+ZVZLbEVdU2bKrgq;7yO@z!cH1 z(I6PDz1C46aDT?5+MBT`^*)LOv z!+_fxAlE-)(7e-IVYfcm$l%^kT+2TdZMQ2$LB}o5L)A9aKd#R24?w4%)|cntJ{eL5CNr*7 zYT?R7PRC{8UCNz(Ns`w*M|0T&L=@WcI;<^%w|~TvPBFu6^1%7$h=BzjZtmhe?%7($ zp!=VOyGM2Q)Ld;zUuU?rPQ=hx-@wgAZufF^cGn=8CQ$E2NaGKWzu}e_(VmHfk)~R` zr_x;KFd1-JT-=FHZwJw&BJOsR{r43n&Z;6<;{v+`V=>w}jK9kyaeQR#$tiN@R$H|w z9eFgEy1Z7X*2lN@l3CA$I}f+={usOBlgjh}?yc gcBQ$*e0@j#0g9ATCw(}=4FCWD07*qoM6N<$f@q4~ga7~l delta 440 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 delta 1062 zcmV+>1ljw;7_bPC8Gi!+002f7DP8~o0Hsh&R7C)B|NsC0{r>*~a{mo@|M2(!26X@X z{r~j&{~vi^H={|a~i>h%BN?*Cq;|52X*5PJXR@&Cr(|CPl5fw})0fB*RV z|LyhvrOp3|z5jHw|75BEOPl{UkN@55|JCUK%i{mM+W)Q5|9_y$|Cq)9dbR&mp#MFS z|1pgJoyq?ziT@>r|F_rwk-`6Num5VS|Bk@_S)%_%m;bNR|ESLYOLh_D0009`$|4TcIH9aK20HI}$e9v!qaSeFkP2MXKH>q^)ZC+fu z&6$NVG+ujpcYjw!S*-i8D`qHDdvxi}P_FiOcZaeaNU&K&*%~W?GnB7Qcoxbho0#A? z9Pp0c&_M;ip#q6Nj64rEwK=}cYMcJ4i`y*^V+LLjI5OaF=N4`u+jU;l5b$|nzYTG< z^O_+KffhrfX#-cO?gcdt0NB*D7P!Jkj}ct}@Te1$E`N$Tx=igVs|OU*XPl+>gT`K> z0J&$JD*>R#o5Fi0tP!a5mhgZ{M+BakG=7Acj$oWUkO(xUHWStjm>7;3CAKFur*nD| zjOoL~Hdfu;A?jfgD~mJo%^A`9K+>Jhh_!<}R+GA27eA3xLEQ{_j)r^gMNl)gkj-gQ zGm%I&6Mxja6xqAJV|=Nvk!?f8t{{uEre=vO4}!`ovMs2Y8ehR{i5xzaIhpm2??e z!NiIoA(6(SQiq8;6$@#qR5mc-Q>hwt>oBn~>VH~PS};+g(m)z17#c|!uo-fZu37N> z?Fxo^hKIL>SNq}x$>EBIoxH=M3k@bsOTJ^}8K=TT5OBVE@m^ma6*GHRG!Ki&}8 zsy3A^vb<9BRpg!i6%a*2FhtzllVHR%@>wfJjlMb#6lW2ya%I85oyR!) z{t@5q_G8AmOBiVjCU_qRELIiI^)Q2g#V}gx<&4h`BF=xU!Aptl{*h>^K z6^v>8)D;61G+7DfiI)DEWS7;3E`2IzYN^>r8o#C5Wzg6u0rgyY( zz3TDr(wF{-5SzJ!8!eC9G2N#M0$rc%eZD**Uj_N9mYTc@6BTsu8y2|7Z+M2?K>1_~ zd`0?GUHUIzs diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 5fc34ce9a95f150c792d5f88b422a1b32ba40456..fd3b01b6d288c9a0d2eae79824afb420cba33e5a 100644 GIT binary patch delta 1336 zcmV-81;_fd1i%WA8Gix*000A=FFF7K1qVq)K~#7F)tFyQQ&$|v&$;)uz4Q-sz+f#f zLBP$i87EPQy3FZxEbd{MW_$3B4JAu-2}`y~WCHGg+}`%EnCW8NGUGO07ZDK`C#;L^ z4;mRzD}xRlltL-}ckeknx24{D4yC0Hi}CkB4$Zyi)ARfD{eS&V1&lGc7be^j>iq_u zlkrGE_C^p1QjQLmh`dGM{x7hMao6oghdbCk5&mmV!Vm)v0|=DA99U`Mx8&+yEXb~Q`Z9ZRYQiA9XA%BNA>>#y;6n1-Mc1s0C4I!+D zBv!23S+IogtDT`xB5r z)J8JGAAj~r07E8(HWPR81!F}Hd(b4-KIl6<=)0SkAG>_@HWRNoGzIQ-A*UzmSWWr} z0_b=yJ?iPPz~LJ{0M$qh`=w4#ur4nvG5Ci8q#ifU-uswPQ<{C?iI{g)HW9vZYucU1F7oTG z`rfIq@=u(7Yfkn=P_B++!6VZ^sa@q6)z!5RrRU_kR&5n}Jz-T_ln4S>K+4tWS+F;V z6@8!;s@S}w9=FhJ2A&Nwgx;_^W@%!qd@0DNBg7ekl}Qe(TXe;r>@L;Se^@qJXBZc8^vQjTx3bl z;WuaHE0dBUAQ~aE8@YVFS|C&%KT`&WCVv&H;+#Rd zI{mT~?W!7fv6*|`Mkyc#7C3ltE7?-h9N!jJ<4s zt=|B?y$Ub7aQhCfc?3-yz#(SRNinCeg4|~zktKnnUBXTT8v>1@i)>tVruYa&$ za_Fu`5SD>~o)O!+<<<)_`TEV>Qnejc+n~*&)XUf`cHEJFpOE8YAv?G595}JjB=V_7 zAE3w;OKt5!ZQb)jF3EGpD0a9}e7MK+-F}gY^V9X>=0e>(X_f0TyF3RS4@PFAR!+|x1#db~sHt9i43A{YQ-WBE-7$m&ZROWEU zw~L$Y$z&%5n2gRnuj4QK;3qx!=F|c0@?k*3bX`}8Gi!+006rnNM8T|0F6*gR7C)B|NsC02X+7a{r^0Y|4N$wAcOy9 zs{dc6|N8v@M3?{Y_y6nj|J&>Tz1sh!&Hq1?|2B{RE{p#ffBz17|MU3&jgS!8IxBqak|9?=P{|a{h#NPk2)c=jY z|BS!?RG|Mlk^k4||DekMqd?oG0004ENkl7qjx+eexTz(c?%J*R;cr; zZScx;n5!UVZg<98%aMB5qa!{U#7+O6`m)?)0LAE3cTq7wbkyz< o1Dh_O-XEAC?es_Z$YwS250E_%Xl{s52><{907*qoM6N<$f=It2u>b%7 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 6928a4e6d8c50182ef8a030468ff770cfed2c937..aee7e4321be4c7803a4d8c12683eb2e5f0062d0c 100644 GIT binary patch literal 2846 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 ws|rC?VLFh+W%U05c-&AcOx6 zc>nYG|L^wyoXG$6`v1}8|GL@#cC!Cfp#L_H|1XRG7k>Zi^#9@R|I6b49fAMV=>NXk z|CPl5j==wdx&LRX|5>B|PMrTkmj4lZ|J>~V#^3*ky#IZ-|9?A@|FqTru+#sm(Ep;# z|8TJXDv19jhX3X9|H9q>r_TS`>i>zo|6Zm4g>sOY0008?Nklm zhOojryaOvCEPwHCBhrE$X zsR95)W^`uwNYG_gUj_<82j;eI3gQb#%o~I-HBXth0)K!t3%aI>jfe$zS<6`i4<>>( zi|PQdC5!eB^t=iSIurb--e=JhdWFhNlP!ei^U54|4xKr(+@ysZuNE7ksrWJYI; zBJ`@v+MryG8Mg)gz!|%gq87@8ZAw+u?@X96MwydYyB$+%dnnPT5ci zXgI#?@Q%JthO>jaX%R+><7b;Y76p9T~tTt5ojLP3^u46U2N7seDq=sYM~ z44!F1!$~PDpfI5D0*pIz1eN52t~a4jfy(KFYJc8QI2w(wbChcNCPT3kQ`n=_hDQB< zPr+@X#3H8ffZCK&)pWfwhME^sszGT+$?u`WlagCET0q`WrWH~06^XN$y0Vtm%y4V? zowISN{Xi+~Fl*32!M4k+D1gS?WzK5Sf)Gvz%sWAA%mR1Uz<-*Dlm$EJIXg<(g>MkC z=zp<*fv``*ZN)W!kzR)d139>5w3{&0>ocz_7hF>33}fM*Ih8*Iu3Gp+&1dQM)*n0g zOtVt_B6CZOX%lv4&$?Vmf@6Ir|$?UQGUETTSlqKoAQL@0l#=hH6UJGah=4VBo z2dw~gST(~DRt=FZV3F4b*RbeBFtvH88$c6e*MlX_fCR~d2P>R}??B!HSkN2D%Y|qY p;!EBsrVJ$GWcJPV$86Z<+uw$~DCT@XL}vg1002ovPDHLkV1g^I^QZs- diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a13129e156dcc8e44937d4492ef206d7725d8548..2d0da17b1782ad67e7de6f7c5426199cbcfa8df7 100644 GIT binary patch literal 4240 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 YfLlQ8cmVrfj76gLL|3gs)h6u!0N77q;Q#;t delta 1959 zcmV;Y2Uz&+HL4Gg8Gi!+000UT_5c6?0Hsh&R7C)B|NsC026X@F^ZyWf|NH&_A%p)a ziT@RR|M&X;3U~kD?*G!}|Ha?`s?YyqssB)(|M2(!kHP<4rT;Jgd|FhNqX{-N6nE&|v|MU6(q|E=H%Kvk*|9>2S|G?b;o5%l!yZ_|y z|JLaL$l?E%#s5H+|IFk6uG0TAjsNZS|Ge4%Zm$0(hW{CV|Ig(AT?7ty000KSNklAkY0rSIPwrVdOf{DCKufRs~KW$fF!+QFcuE9w8H|;9mSJ`uoEBwI}I|hbAeB=V`9e9(SMOszWV;7h7luKoh7LW`6zkq z>-5{x8wPGK>5s3E(2#0eKB&~vBlJ8{&vRWt4qC0!z>|WOrPI(ZRj|0vYn#SOv{IT- zi#seT@=4$pt%3yfAje`hJ_#jg1tb>SVG(x^mn3$B);-O*Of(#va(hK{KG5=LmKx%~ zOpoTR(0_7ikDt}u<#y{l69wSCWePJhbIR@g-`NK*6jsmJN-}bi=l6kG7R?A&iXqZ8 zn#Kl$Pafk`>qt$wVywMw(Bxo=bTpqC@4-H3dL5*pJf%X=sdf>wCQk@@%VY-ww{im} zE8n5lxbaw zI8b)hy*M*toCPiy1^R{3hB2x>E;gs^!Qea_x1TzbrD(f!GV||TP94>be&wb!S8pi^ z+O9H#IW9A%sOD^$t>$o%5+ysMpY7@_Fn_r$C1YIZJxfUuml#np!^K*Z1WR0?K!NAu zV!lVo2DT}fpizUroV?Np|`E(M|Pl($G_p3dkl zF0C|HL>k?lni3Sf@Xw|=ONQ=KQ-ApmTps5MQYZqA_c}!l{Uuzf4E3{=cz+p(2ezKk zxrCs9l-P3hZ;vUOM7T(2LQ#Z^?38a^ee!n{t#Fy1tIMoIhnFDVCzOqxbZAz6jS^q~ z>eYfVT5c~uPLB@m5@wgU*pTW`ADq8;X);c>j#K64jN|z!ynbpKoTS-jtXF98pBd{R z#0g#l3RII`yfR*O1Y6A_6Mxj9XZ_KM#c>cMp(vG+I74~~=N;_T#s_L{=c)|~hDJ^B2P}ZZZ zpdzG33bUPV!XTxVndw#E*qS13HROdU?Kjx(eOl_tIihG_;6$dtlYd|6=db=kf4QZV zIX(p`J_!~?-ih5aM=OOT9D3{1!T|sBY2z^mr~C&dTb&kk39k7ACMurNJ%X-*bEZ3C zf<69an8Z6G72~OO(8yAyR#OlaZMz3Nt*)VQ0m}HOuZndrZHLg^bp~i+AIDR#h!muH$JYd)9U|z;wFy7 z$*^H?6P~)OfSHG;stkS4f?n@d9Z;y6A_%@&h!XYecYDBVq36kYI z`_P2K8G4#b|DT{)!rkcd-=I0hod~mEL3<(Yf+_GTXy)H>2h56pf#dT7C#O~5KL$Q- zoH_7&(09>8%!K6rJ%p_N+7*cap4 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 229bdf563dba4a57d01b59bcc19522eb674596e3..76abd423b27687314d56951abb83d5f22006ffdf 100644 GIT binary patch literal 2594 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 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index caffb26a36f60caa3ab4f40d826505730fca1ef8..e08138333486262d3a505ef532a2827709005d41 100644 GIT binary patch literal 5794 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 YEi`7===YERv1c?@MNI{SoK@KW0OTwX%K!iX delta 1686 zcmV;H25I@CEt?IH8Gi!+001u>&=UXv0HRP#R7C)B|NsC0`TYL{bN}`F|J&>T4tf9l z{r}A4{|9ydu+#q|h5r?O|4*I&E{p#ui2v{Q|Hj|{I+6byfdAs}|I+3Em&N~IrvF=` z|38%f=kot`vj1+Fynp4#$YG^idA68A_8^6P)48{3|ko(P#DY&NR^s5T_Wl{V2|$7hi*IdA;+-N6k5C8dw;#sT7UJ}gK+lEEA8|>Y-F^j zQ-lyn4O}{OgSDYThq`f3SZms)V;R#Aw5MYy zQw@PGX7Yav$Z3}pZacJb6Q!qvy=(|do!V|huFm6LzwZ$}ByjwGsH-xT^ z&s1Y!xsW|TQ|F%P?(uHK4B-HJsy$|y<6VPU!Y62PubE|!cL6gwBj|7!%+$t*duD3Z zp<=~MeSA82%+#$y!4tD=C-~;B#Z1pGtaX}9=YMa0Tt=2&_L#IbXPVbl9fDx+95PKf zf~8xg=nZ0+boNcA=mxNI7t+XoLaJc1rP&Rvv}h9UU1aGbGzuH#%*gHm=XmsJlAyS_ zOM`h8=V|n4@?w>ulWlOecu9k9jDq5^UAf*FOE%DMup6blHbODMD@AX}$xSF5p^&NC zY=64Cr)Y^ndX$W3dh4`zg#zvOwBO)04(rw0lau+Rx9ctlPK1vULcXCL{vTmH0- z^{)>tOn@>IrckAkMFy}a@XIo+uD<${u zg>@VX?va<Z>tRO<<|+FrAaU;6&%P#|$kOwx-O~y2CfKzJJ3k-45(6 znaO#;r|La3Ic+GoikNMbryd+Y#c|9`efdHv+dxVEm09L^H_A~7dv{G{h@`86YzIXn zhv|GFktIu@>SfDRUGwR(NRF$xC~ia&B)Pyv*^?yf1AN-n{3;j^xEK}Zd%>FRiz!!^{{zcZ zn6|aUlY(&<;Lo{x>5po=BmA*OM5$opqK-dwd~;|h7&#vX_!C#PrNi&Q6VCH(t>NM~ gRcg11#}oesfRbFCP7h8}!TXrT_o{ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 751104548a96b25993043b95b8ab079875a21077..46de51af67e1c4ab5b129af9ac5fb03cad3e972a 100644 GIT binary patch literal 6468 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 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 066560203..18166c8ff 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* RustDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RustDesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* rustdesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = rustdesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -127,7 +127,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* RustDesk.app */, + 33CC10ED2044A3C60003C045 /* rustdesk.app */, ); name = Products; sourceTree = ""; @@ -212,7 +212,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* RustDesk.app */; + productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -462,6 +462,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; + PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -607,6 +608,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; + PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; @@ -644,6 +646,7 @@ /dev/null, ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; + PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 7b4d860d6..682280dc5 100644 --- a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images": [ - { - "filename": "app_icon_16.png", - "idiom": "mac", - "scale": "1x", - "size": "16x16" + "info": { + "author": "icons_launcher", + "version": 1 }, - { - "filename": "app_icon_32.png", - "idiom": "mac", - "scale": "2x", - "size": "16x16" - }, - { - "filename": "app_icon_32.png", - "idiom": "mac", - "scale": "1x", - "size": "32x32" - }, - { - "filename": "app_icon_64.png", - "idiom": "mac", - "scale": "2x", - "size": "32x32" - }, - { - "filename": "app_icon_128.png", - "idiom": "mac", - "scale": "1x", - "size": "128x128" - }, - { - "filename": "app_icon_256.png", - "idiom": "mac", - "scale": "2x", - "size": "128x128" - }, - { - "filename": "app_icon_256.png", - "idiom": "mac", - "scale": "1x", - "size": "256x256" - }, - { - "filename": "app_icon_512.png", - "idiom": "mac", - "scale": "2x", - "size": "256x256" - }, - { - "filename": "app_icon_512.png", - "idiom": "mac", - "scale": "1x", - "size": "512x512" - }, - { - "filename": "app_icon_1024.png", - "idiom": "mac", - "scale": "2x", - "size": "512x512" - } - ], - "info": { - "author": "icons_launcher", - "version": 1 - } + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] } \ No newline at end of file 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 1c6cf008ad8720ee4680a32de29836e8d31ed7fa..9af6f2121eb5a5394d671f8e0ab600cef240b3cb 100644 GIT binary patch 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_128.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 8c2837a94be001dc0272ce64cfad36886903508f..493ec70768563bacad4356d5b0de3562d835663b 100644 GIT binary patch literal 5475 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 literal 2409 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 delta 322 zcmV-I0logx2hswN8Gi!+001a04^sdD0B=xCR7C&)00TUT0!5wxF>eAnfB_�tgZU zTFe4Nm;f(%812Jv`F>U~H|NsC026X>*vj3#b|2~xe8i4-}c>nSE z|Ig(A%HjWcwEy?||MdC)f4Bc=tN$~O|1gaI=<@&4=KsXr|9{5B^W*>k01$LiPE!DL zgl%j+1+;%}Xho!=Q&N{R^t}K80D?(GK~xyiZH`wKgdhw>$H86$V#TieU!w`4M~}Xr zOP))>&HTTXpQ3+h2NzTe UgN~k=m;e9(07*qoM6N<$f^iv?cK`qY 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 ac1b107662f7d9065b4bac6dfe07e3159121e3e7..22893b8ea78c8c2d4dc1835a5e19b3c056e11d28 100644 GIT binary patch literal 10828 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 WX8Oy%I)Y}20+i&`WNY5Q!u}tVQGC(> literal 4616 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 b*WWN4QtqF=Dsb+fjK7(Yff<&phmZOnXjx(N 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 9e593fcbd46695dca489df197dcde05b64211cce..583a485712bcc8691458c7abb38b37906ae6649a 100644 GIT binary patch delta 1549 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 literal 9733 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 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 76d846c4a66adbeedbed97062f890cbc5832570e..f98ccf1f3eca7f764f5a066ae22f6b88fcf6653f 100644 GIT binary patch literal 2851 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 bn!n8-#h{tR4FTb;00000NkvXXu0mjfO=(GY diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ebb105178..cd618dfc4 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -488,6 +488,14 @@ packages: url: "https://github.com/Kingtous/flutter_improved_scrolling" source: git version: "0.0.3" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + url: "https://pub.dev" + source: hosted + version: "0.11.0" flutter_lints: dependency: "direct dev" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 95449e611..8701d9f5b 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: bot_toast: ^4.0.3 win32: any password_strength: ^0.2.0 + flutter_launcher_icons: ^0.11.0 dev_dependencies: @@ -101,21 +102,21 @@ dev_dependencies: flutter_lints: ^2.0.0 ffigen: ^7.2.4 -# rerun: flutter pub run flutter_launcher_icons:main -icons_launcher: +# rerun: flutter pub run flutter_launcher_icons +flutter_icons: image_path: "../res/icon.png" - platforms: - android: - enable: true - ios: - enable: true - windows: - enable: true - macos: - enable: true - image_path: "../res/mac-icon.png" - linux: - enable: true + 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 diff --git a/flutter/web/icons/Icon-192.png b/flutter/web/icons/Icon-192.png index 5d4566850a8c402c3ca0565f20295e9466361d9e..e8c754f4ad2127a599473544ca40d6169fbe35f0 100644 GIT binary patch literal 8908 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%0D7IE-;TOZw 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 bECTt^{N{VZG)-Gz(;!_fgDbU{>?8jN?wrh+ literal 12570 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 G68;CvQ0u<{ diff --git a/flutter/web/icons/Icon-maskable-192.png b/flutter/web/icons/Icon-maskable-192.png index 30147e96ef31e6bf72170d69837eec2f42d4b8d4..e8c754f4ad2127a599473544ca40d6169fbe35f0 100644 GIT binary patch literal 8908 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 IM6N<$f~u99dH?_b diff --git a/flutter/web/icons/Icon-maskable-512.png b/flutter/web/icons/Icon-maskable-512.png index e84ca5bc7a212951765d3993264a50517b8fe82f..2f8929e267b44bfe9325ba479ff131d16e2586de 100644 GIT binary patch literal 25973 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%0D7IE-;TOZw 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 bECTt^{N{VZG)-Gz(;!_fgDbU{>?8jN?wrh+ literal 12626 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 literal 21592 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 diff --git a/res/128x128.png b/res/128x128.png new file mode 120000 index 000000000..f69b60eb2 --- /dev/null +++ b/res/128x128.png @@ -0,0 +1 @@ +../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png \ No newline at end of file diff --git a/res/128x128@2x.png b/res/128x128@2x.png index d6f8d20fa045eee8541c3912b49428b68af98324..9bccf65bbc430a5661e689143c553bd2e223dee6 100644 GIT binary patch literal 10623 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 C;{ApI diff --git a/res/32x32.png b/res/32x32.png deleted file mode 100644 index 33dc805376406f8e5ee596dc1484350b1eeffe8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 493 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy69RlfT!A!0;eT{g@L#6%|Kzp* zFW>op=)(VwrT-s1`@iet|AgNEO^g1&`S2eIzW(^nko$k{ng4(O{y%>Cze&~q<$L}I zwf&#I;s1|c|37~H|MJ~`*T(;wkNlS}`~Udm|2dog*UbArW5a))ivJ6@{eSxU|GNGE zBRc;V&G_%z^8f6O|8k}O`&a!}yk`9n=p^=%AirP+l~wQmzg^ir;ie)310$=ai(^Oy zGMa`n|T{tWZ_4Bqe#( zj91f_ykC6Pi_=9iGSJl1+-J3!tinyc9rO4dC3+hz7O*@}ah|}GvTwo@^Pf z=Uf+t*$+fFOtw0}w$8MI<-?nA52rZFh3d|5urlI5fBpV<69)5FjqThq_tX{N`8Rw{ z(EY-YpnjCW#y#xwT*jHp8C%v1D(ILheD~3~7xZ)0_NPm3Ulcp}Q}65cJJ$=;qYj;v hTh_$uerzE>1B1vdse~7o*t$XS=IQF^vd$@?2>_Jq`C|Y8 diff --git a/res/32x32.png b/res/32x32.png new file mode 120000 index 000000000..7c1136a73 --- /dev/null +++ b/res/32x32.png @@ -0,0 +1 @@ +../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png \ No newline at end of file diff --git a/res/64x64.png b/res/64x64.png deleted file mode 100644 index d93638e6eb1d82b24807d7b3d0a6ec5c72e0bd86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2264 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 GA^#74rn9U7 diff --git a/res/icon.ico b/res/icon.ico new file mode 120000 index 000000000..75324b38c --- /dev/null +++ b/res/icon.ico @@ -0,0 +1 @@ +../flutter/windows/runner/resources/app_icon.ico \ No newline at end of file diff --git a/res/icon.png b/res/icon.png index 823967c49a8f3440fbb26e2bd05c96c78b939141..2575d80e7bb285054ef964780d77b4a9a90d1491 100644 GIT binary patch literal 60426 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 diff --git a/res/logo-header.svg b/res/logo-header.svg index 40c19c43c..9712636bf 100644 --- a/res/logo-header.svg +++ b/res/logo-header.svg @@ -1 +1 @@ -RUSTDESKYour remote desktop \ No newline at end of file + \ No newline at end of file diff --git a/res/mac-icon.png b/res/mac-icon.png index a9813152ef248d07076f956a3642575c396c0811..b6e08923fd8fde70dd059b76976ddc32c35749db 100644 GIT binary patch literal 51695 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%0D}t}!A64# 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 Te#RJt2VrvD;u!IW+pYft1QtW! literal 90116 zcmeFY^;cBi8z_8c00BusLWz+QKD3~K)S$E=AtepcN_Wp-fQU#5h_rx6OE-g}NJvXd zclS^;bKZmB_x=a>$2)5YE@tm%@27J|>S(D_UA}%9001g=HKj)YKn(qt7$Ca@{X($` zynud@yQ>*{0RRR4#XlJEF^w7eBh2fO>V2TJmv!x8y~DkS_W+CU&)Rpe( z`@=TzLEdJkK4Yibc$t+yM0~Nt>aU+j^4u3{$cugS=VQ&HOr+5LfU(Zv^5N?=(vRXJ zxmWHau@%_`xw0n^zvA{QsrTDdYq=I=|DHvo3z@<5`>m;w7x>l9Gul_o&v0|CPL8gA zk$PA~^*vwV7_qwSdPwNOv`w^GzIXH6I56Hy?(+ zal6+X62b(N2LO7Thx^3e#JNe)ol0v*;jP`@y;dFh+}f0~ZIo~; zTr2>z-(%ktc})@4()XM&r56GK=;nzM#uL1Ikb&A|AWszz@TE2xA!+v0%_w2ubJ|TJ z0R7}~UaQH;+0R&J|8OiU%S;Uba$F^6{W$vjUf})6VMqXArm9J|x)U72&rNKoP6p&e z69b}eGIkAnK{c`y1U)eUv{m_M}yS2}Z3%XRl7wr;#?}zXbCAeWHKS>;F%-5B&}f_Fo2b;Z2zShW!YDr z>?VlFv(+Z_ES#%LL(W$LEP@=cvOK|6AHHnTFq2w4o@2ZRVb_X(OtDjtq&#*L*kqyu zT0{1yiwMDFXG4Ok7(PfN#vImGl&4n4xq-h3=<5ypeEb|!pZ4^}#IphIm%k5rUk*V8 zKywzqBC{U{KLuM#DOf5S^N;oud?#L)8fa6GZ6I zH^jj1bz3_EQt~k^0t~T^zqrq;&S_e1g}kZ5`SaZQ4m&p`vGg+ z6*$QX#_0ofwM+f%=tzho^uI%~A~FF_oH&~31b6d)L4>vnp3=K=qbm*2WXc}&V zmK{x$OGdBgFTkkp)8ukxVlkW>+;CARHNfio%T|zg>BFB2b};k8`)Gcu-aVzkA(Q%E zaie(!zvs?A_(i;HY+pNmSQ;QejQ?12D0359_rd$wybS7*yg5n>O|Ae37RtuGJss9K zi3re_yZaEvnA%NH-9^ZfITnBK8PHBm4!kXJuE4C$Q1jxaHh+NhaA;*yMKvv(P&ag3 zr4X+=b6UUOZKIVX-Z^h%C4Npxy zCDP7NT8}qKFI23OBkW-_%dI#vh0{2om3cQH!BFaSG&6zsK0BHN!X6kSB?RnEAvrQj z@fPIQ-GMv}VJWiL^w7cX4l$TX_xxhrCWV}U$@#}%F;fnq8iV!ibM35`YCzVH zCR-k3?GDm5n&tdg@fQnO-(nEdsTt3v*MCp4X%Lg?+%E_67E1nF9+mw~N#u&oi~V(W z4Pq}qJ<`M`z1G)R!D>uEZP#Rru>f}WPcJ`c5A7E&!u=jpj$Evq3#+^!$yN0wufc7c zA}CP3qA&EBlN{}N1BQ<8(84PtEF%}cA;6bk&XxZG?fZB_i0Lk0yyj%T@ukAx4>yr$ z*Q)uq*9VO+2>K|)cikyJQR=&pprWscihPt6@>|?-U?@DDTyJO`MZ14WqLlx6St~C=wB(< z!_H7<9-c#%ChHK#)HXSp8LQpdu=%!jQ1I-DJU~q&4v4}x7g6JOC68*u5metm%+-Ha zeE%`@MpGZYc3-lArgQ!Qw=-EQ83a-Bec8#gi2{t}{#m6j0fDdFjkdWU|ND%L#JV$) zrEfngyStx|S~L!oq7@-48lh%4Ziq8C(LFUj_4c3Lp@7zkTJ(n3Hfu{-uHtzE=vqAi zXVTWW+3#%}Hs1hkht1EjVOvIQ@G^_Q?TAgxQ;*Ud5+qM;5>mu zzLi_|_;}H9f|B%!aO$f>7qYKOzy#cV7lN&3+?_zsJkAz8aTr8GAaBO;z}Rtn+9jo> z(&p3AoyC{Y+yFHrIlwCGmIqptY|cDKar}dst9LAEh%i2J*w5=zl3)&!JaLndyLFwK z^4d+-8|NJMDZfRO2F(sVi@?{8Pl!M=!i6XeBb&<=^Od`$%_$$buNL)QNkigTq?F-uCfvx^3(2B zNHWc>@zZR(B7znY`&x^2F0gxyT`OQ^>r)WLLZI zA7qm;O|*j{SWzsW_(pJR1g=Al4!K7HU|-D?=sgXe#W+d+LlZsPXAhUH*zIFv6b^s9 z<&li(!FIh-*o;s^pD!C2{9S+kWlPLl&J)MsF+>bPcWJfw()4UAOtMc{e`Yte4+#bs zQ@K@@H2eDaq&xurGF7P6x%ov9jMu;(8MnmM4$$a-=bS?d$TxG6nPj(AkqV^HD-&&L7#(rVxbi)bq72*-{-%hNyG)`du99%u9xZ$LiWb5?b#y1{v^e7Zp zU^}|!nYc~r1qryk#FPCL1gL!Fc<4ewtiZg<9dq0=xH3~}R=3F{l0GUDY<+9E=}NK^ zU?E-u%=+wqe2Amr1qB2IIE#w5_lb~%vn01P=(Wsf)TU=kKILAd%`x@y#o zQ=>=UMUj-fAOT0-Ty+&Lty6UvofmWkbR?n(_r0wDbCEc3 z`8$WN=dEgc264)wao5If{3gD;Lbz)AaP34fhW4K@yt*q8eLD00Z0}2b!+LIg@#(qO zCsdOJH{|V4nNp(|f0S7G|5AJ;SHXEf$u~J?s!LnQBZ`W>;t4_Ya{Ok~!+Zu{hJ}gT zfLZXQBf`8`*_n z?l8&`PTm9^YH*xbW*QXqf^z?q44{swo|oUXVUlFe&=u&e9-E;*TL^-zl9BB+vf73J%hObYhg^kBDO9Oq~%wjXNzJXKfq zDW+F`F+;1jMVX%$rN@Vd%nG2Qc{Y=FzAggd1|$uJB1RnamSMAru|R8gN2t~p!T+B- zmOIh`liJ|tFD=>^hrP|PKJQD%U@@`*^*(nKhx4*~h;(>h=;TkgiA55r@RdLMD}>EH z4AoHKgU+?_=KqYd)l22-J~R1KLaprC?%L)J?PMO7WvagdjI$Qq-2v78^8x1gMm31vsXFcHQ)=#U zE6~h56M2=5f;K8dDj|h-aJ%P*BGqXnq*Wyr*iHonW;$T(?x`iUI-18g(SX--r$J!m z*a%4%>W}+p>ve>6mol~bnpf&%uTvijmu#lt^hf|YfWGfJ7)z&L?`ta&SH()?#AR+HgzQA0>3nfXe6nXr$ z>_Y@}J3f)<&0sx*CYr4VX!2Dj2YZc=>%^)eLiD#ghQAoe9r;z@voFqQe_@F7 z-0GPBzBIDrEPxh{Dj_+nf@|J{Xy9=CDuaelnSG-cLje7Z5GN;=my?O~{t3Cq!e>HI zz<~{~?XpSL2RD*_dvOioSmF<&+`vsu!~G#=X=B;45z;>vnZp8ZgjQ2!f%k+9o4=rT z)FT!|*ik!vEzd*^9O1j;N??6)#2>h!jWM+?pmO6Kzv+_NQJVtfiC+ccqM8J>CuqnE zgWpuy>swkFy4#(4B*G%osl;fk;&*Me-#gDo2t#%#ab;vGCBlB8$ct%4Xjl?g_Rj?V zK1J~)hwk+~&}ZvNEp$2v!$?q)qk{#rzA(L&;|FfOJUV{KRGY9|(KY|+F{lk<548Bb zF#l}sO*A343GWcf&kb+}nAjQ^Mw0SUYM;F9PdABqH5}!-R0(p+i~A{+Vekv!wg2&e zxV(Di7s|?)ke;|YnQY>kU4MOP#)`3(`vy!D=G^_Ow?%v0|HN31sU`HB3Q35qJDXv= zz>r=EN{J6p4hPG%3De89-bWgJ_Cnuf_TcL1TdHs?_!nV^bNSR{!-@isrOowkl(zT@ z6#fAp)H;9dq@DGr))q6&Wh z$0J29j=TBD?d%&^y5p)`5n#r|#PpMzLWo>Jpw*uXCOWbmuvfEu_|7a^t3fKhUG{&7 zaSyGMy2+jx&F~KNtRBmsYd>)^AJO0-zrp;B8)*NqbyyY34rvP_hPr1TzP(oFH&r;s@q`g4PHCkX2QX^{w7c%@G1R(S3$x%x6Q6o7 zY(@|`UtFjH@Rwm2O!_JiF1M$QT`v@m_N*zZMW~6NP!k6*iX9Snnh>r2c?}BCG zBvCkILhfGog^U3rKBlg?4P@ZwgT|5x`!tKwu+)&Cei5O79!`}!9xF{9m}od-U*_oY zD`gR{d9CI(74FGtb=jY5NN6)U*-QUj2>458Dl|ARM9=O{4E@0a;4M8JVACEBn%6b0 zV%(j0YSJ^D93EVFJSL^7)Qh?*V4-Dp-sh&-yz|oeCG#y-?yh7l)ll`O+m*fm9ZZZ} z4R}jc#&Oc;IzIcZ+tb)~EZVfhP;l>X=IX^}e>>GBsvUxQN(qlcDSPB5Y}k%U9j!lh zSybX;<573_yz@!sLqhV1G4Fsq_s&T@vTyN%nZS&mK7QlZC4**ZaqpGsexLo-wT>rm z(@OwmB1jNa#Dj#H7C)KdB`x1+8MrD{=tiynb5y(6*_wR_yoC9^FW1fM@6@pf6q=9# zF2Dksxb^YZeUNA7yLB0#upym**~kZFoBy%WqO$8S!qwGT`B5{yF@u7H>F=&+ zCpa?&bQrpsx$=m$=FIIgT}jA+ig$6~Z*{ZtgMU0JOH@J02Ci71W+$jl=cc!_OtF}F z6+jL#nC*rEU-cc(75)pU5&0o$4g0=XRog;lI$M-PCk5*tw_* z844@m7?w&!Ro#!U2XBcL3Qmiee%N0X(hs6iAZv0 zRJar0v(WI>ec(=N_q-efV5b7l`tzsg&BPkY?Z7nf>2j`&A#&OFtOTsT*w$+CGZD!} z({fz*gC))YUff%KA8zHelFAM1dPA%bX^a&TXbW(iGIyH!6Y4F{z*B28u!#KUTgDo5 z_QS#5vhJ+H!NwdqYSyjUfpA!$NEPh8z93(L`w4jRAfq)vaWvg2ZM0~#g|U+>6Zcih zPvzp++0Y+H7zwU6@Rv$eN`_O~L+w{De&sV6DJ6?U5Tmt~`H1;g_<;u&Ivf0PNC@Y+Dt#~8N!=x3Ha^Goh$fcT5>d9 zMFoqw#fV0-p}zAzYN1MIM1EPKx5+JI_@5wRd;a|G(=ad-VQXJ$**RYvh#oqd7X;o@ zk)y>bhET@wiLoK6nbV%U=B!hNfxN$_v6dIE8>(2zSjq1Fvd3o?LZ%ew?cZS!Q}Z&u zcImM^H}K%|IAs{2w|f)vprg$LUXt5?BW?S&lpsTNvG;4H7C39k(j~gBI;>`2s%*@H z)*;bRf%e%usSmp2%6f+F9E!lwdeb=PTCRXeOR(}2C3xZYzV^+pXHKo2%KHALJzj0g zSA<&`J9DM<@Y27(M2_Ac_>8~H>+1egMY~JAJcBhdhH&>EiSVDdpEn(d;AVTGOIB@5 z*qe8qc}!$6xjoAj7j?(?Z>fN)j8s(z&<0ae!#3j>BB#ELIxJS&`&S|Yq{xQHkpfPV z^}3j~hUg5_Q>ILhsHl}YyUz!2z;rkR0(v~j2vGQLN!U8^4_VEfaL2EahC<~rh{aiT z>kETSepP|Lsn&`7LPHFVxTADmTBDcl!Lx4fGbP%MJMVu^`(QXTGvcIfQ_w5>k3#M` z`Y|0`sMDAkf}!MfSL(_D+x@={LAbp{zTgCYbUbt{duV`hm5;U7N>HMj1gY!nkpD&) zBDAt@5&Re0yT`7(`3Ijprpw#PZrtBWjg$2jDuQRhvt%KBRk5E;@Za@Zx7rW1=A#|C z9&p~>ziD_8kpNQu!l&Yf%f@~O|_gm1CY4ChD+j3!c6VVbynxgyA$uE0G z9;e{nvN)TKT5r`QNJZ96AM*A-`rmHRVZ9iQtEysO3VV1OS=mjRye(V|Q>+#tQ$ZdA zdfFyPjJh7)ui4W2^X26bxK@eeyCAX7`!D-#K1{0O!`J-8f)k63auP@E%*zeY#Z+iR zw5|%=>d=XBxq&a)WpMedM6G6Wz+R?ltWXuy`zITpzOXxGp6?-R$^&HBU%u$vEU!N( z{V!lxj0L6w`Tr{`FP1C5$l{8-O2TA9b4$hb7!oC0T$1+xY0s;o44n9*6i=sN`}kMf z3Q}Lp%*}ne#=#CK0ysMy%wtG|{O#YAr?7e2f3on-Pn9kR z@)MS=x-OskaKL_u-sYWXM%oqoAuzdZ4RiG8cjiaknUVs8zEAfNXQVdn^##HT1Ao%v zNWmY*sT#h;fr6~MhFnFDUBh2==kchMa77Wfc65b1ob1B80v#`z4h7mYE(Lc~3=b*s ze8M51?X4Vl&-!DWUGqF9u-g;McN`eH2R{zEKUaTy_q)HRO47t01`;%e3-$6DvoN~m z{ih3@q>;n+3T|AwI6i{o?tcD;ZdxtuaAUlwLe<69*sHVs1NkJNLJ!ev^s;dk_N*8? zRpNM4`kLJmIGk|!Pj{tcucsq=TkvC5J-iwdaW5A-^GC}HdSE&Xb}X%qoG{V&U?VPC&%Aub)%B$C z2wiSK<^2(1W4|H;n11g3zSXwM0i9prc@5+5^j~*c^;v3Q(fZU#P0Z39reha*u{%N}BeX1cN~v&u zv(9vdz^?H#441)9 zQxw;la-34)@xQSDE!rBtNaO71rkdK8!JbU-Dcx`_ovC~XHFf%f3_{!U`x?s2*vO0vN(+DgzUy?w;T?a*hq#66|1>yb z-lR3ic}q@8rn~uls&W4HDCP72gp+K5Yao@7gXGjjHdIbU=F0!J`3)5SgoogwPoI2K zD;G=I0XH%u7dOoPnv)t*ta+}tM|P=OnPRFfnVz>eK#{ZakATfJ0x)jS3bT3L;$*ip)~DjuZ)k;2%?(+WHAIXUJu0ViDAXikz5*Q|yeUwJ6hNK$}Bt7e;9%nRED72V&aZ(0UFJ2@M{Veu!cK?mA&u(B8uLJuO~=w)|N zIVcmfy?GEBb!wAz^>H&D9o({8fMinwu*&>deQaocRat#z>YDeV7O&^ZyVu2PEo8Cs z=m98Qu5uK|?d7Q|XIM1mNY+}u{v6Y!PC6x1n)>^~?4LUxFA|8D_7j3+j%M7Fh`)lH z+Mur*F)nw%np~~0d*bo#WR-0;ttNtC?dT8|Uhq~+$5gZt+ z3?d>-#p9bZZeylFjBlX}&lUnFI1J3Jg=*t1%YurClYH4LxzRmtS@)sBVO#mZO5PD^ z$=br5+B?NEla9WJ8hxMVcuQ$o-*1+2PDy+;6N=OsEbrcz$OfLV{nlB)+$c#ZT~td zXM~)_47S`o#8ltbVVD3^00w}Iix`A-HDL8)X^5A!w8@ccgSe1sb11i)bRVDq_>(Xy zg&xY~cyxg##+{8RKe$^5W13Dd7)rs&Dw#iV>rf2OAbiR-81>2buhF$PsZq;|T_vRn z(`)*I`!v}YPh9XSWW8SRKHuFzfoY{9F&qD_bseRer@wio70$++NKXk6ql5mk9LC%y zKef<&LYL+I4fH$f4SQ~E*SZr=37|EvBKwbs5<{griUURmqtzDwC=~VunfkO;DY3Nj z-5?c*Hysx?6jSs>x%wOx#J*37a>zB9bXP4DW7h0!3_Zgn*AYTtXmSbadti6UdtY<~ zDL6yX2>@jI-7|sf5u~GpS`SJj%suCZ*NETk^!1o9#m&LcK6Cj-ZIL*E1FUV7dEHwJ?AC@Gz;%-Z6e~n zf!OMT2ESTTw#4Zq`Q{dePw5&X@L4CAR4}BaFOM}WLP+b6(Z<%Mue;XpKPq6ugoPSQ>i+;LR<=ZQayOIqDT8Y1+*SKafkByS8V`oAu zz~&mx-6_lmnvX2JnP+Its-Of|<~duOhy`81K9qAW{8xY}*|?-D_s;AOeQkiPX&##= zz_hGym?T7^K+(zp#W3&+&kx6aYeSVAPwd3RLX`3cTc{(Tq}xXyu+lW1eYP~G-Q>3a zoUmidyM*28XUz!!7*DpTf44qT(!Q4uc{s8#}-FhIIcj zaAtpcSDl97H=ZS{LHw4YXtmXE(p=~y0%f##94hPSYz@` zww=cCe_tx11L}gbHy_QVazXuI~j;l0%CV^&}+o@QuP04P| zpg{8+N@W}B{4IpAS6KNW@k&3l7w3kx9A1kXlB=D*l2WQ!Kc{0}c>Rd;5&-9uq;O0M z{m{BxU4h~)dd+t(i2gOr;h(rAtahj!&i+2YjCyg>am7QoO31WaKMz%r)X9(;#0q@KAh{XWF*O#Q5Vu-3c*`xesR zXNcw*Wd1^ybv6F%+BK}1P-%Vp*H4n~{upTe9T<)t;=oysK!NTHc22?`O}bJ2*AFFa zQURQQH$H0x)^h4Hb&ezPzA!Z1*C7iE9YLLddCo8pU(vE#!4d&d<@~U)k&tPFe%z)7 zR1EclY}DY64*k9@wR7L^~L~j{`5e)OJ`C&K1h)W=rW^UYqC6S%{v8f^jnD3RwR|EP(k3(2h zRU78DdS5E`o;kS{0S4*-D^%Bw=s1VkbEYspho6Qp$@NGmW_JluS>jva1ytUV`1-=@ zO_MZdQKHl4^K^mF!50o;n4ULdQ|t=-pYX#7GBX zb|;H;8ol6Z!~iq7u=G@B;Ef{NQr>w935ncclm%2SRNoN$214rW{^d{EOQc9djCGJX z-di;<$yVe|2BNU;vRgL5+7Lv9x^&Kv|6A^mI6GotIxI|o8IaN(IE+krF#Kz)s!Yu1 zzIKMx6?S|B)Vgr$G0F09eDaNBe{R0^YHL7!&A6v(;jlYKNS- z=0gHfZrwxey-PZo!EF}L(i-o&WfmDhv5+`;8Bo4%i12s+pn~F4%op3;Zm;pn*KRV; zq@D?Cs22=i&HW9RB8Cj#x8+*>cbXgNGA_KjtUjdcKk}2V*^u2pf4_1P6MjlZI$$Q5 z6Q%^974_uK(1+iRn2Piz(!C3}Cu+g;j53Uoh)C~KDI%T|Bvc=#PPU{(6<0avZwH%l zM}Cju-V^puk3b;ype_CLi7u}%HNzjPt|kN z-H#ue9Slom#CJb$Yo|oFEh?Lq+)b?aN#vU3uYrG^jJnUMa2_o#4hKjAHrWDpcJmQj zCkO_+Q1-fTl>{DG=*|i=BUu*NK->d;kMw)L)G^!6BgPY{k4~FWiN~N90p+L$uzq$w zEt?oE!u}#>Nho;4_L#G^b0EMFfJ?bE(vG!$wFJrG&vg~y0O_j|7(i`yRk9*`xx6m0 zx~#UgAx;c<{(5a$O%v`9-mx zXW*#MuVQK{c_@3y__|*}q$HvrWv@}}d3ybo0&0D`?S-Cr!;>N)^$)1f;sImTKyS?kwwi`FPo@$+vU zXQ=X1w%OB6LXen3EV&ooRNxTw82Wp9ZExV14EW2NzLqa%RI>SZD^g=p(XU~Eiy&(& z3yV-H*|5`HTsty$Im+w{vZ6y|#1y&^k&&Z6K;44#?^9Q7?n)qsT%w8$uJh~i2%KUVp zjGHFWKTo}xEw~L#pqhDd&^a!EE9O(I?R6Tg_={60?9^BqyP|&IEP+}-aC+*xx4wnc zJkzxMt!iV(>7Yw6b~uDOx7k*M)ad@fkVE=UFq$uN0vp}YNa&aG7?!*YY=)6o8E(66 zw5%S8)QVlQfQq&h2QXmMK>x016`f4tz1J?zlg59KDvlo3S}XS3<#S$t1!=ol@g;kx zi}s?#DKXSsaxz2&U$cP?jL(%70kHOdeUeKA0NdSs0Amk|09v1k6{q{#kh$cFuiBRP zXzD}tx_&5)@9KIp>rU&)9zXSVbhyaqfTb;#p=k7ep{Ci)cH>DEw9nS9ABDGhe;$)6 zcGIJd&~AkFTIO}6fEvy`N8vvuI7;{QBK=q@IP!Zn&o6wJw};0q#G7~DAwPU|+ShXV zKXh$Rpwo=^qaCN7E}OT_I{Me(WV3fpf1K~@fxo8zV-{B}m~SPUaG|g=1<*!$1999x z?hN04dcZK^0UNavzay?m#xn0oh@)eSr|; zMN3OP)9@?3{*v{TVyf7vWh9{jXWdr1_h_Z#m-p!a>FR@Yz_w?djKrag+j( z0Trdn2d4|T8K9ZRmp0##U2irRjz3)oJ~*qN)S1r7ZrWLD8@=^#BfhutEgT&=X*9X$qk%A+K~GIg||7T%`W6=DXsr zcadfrc$vhbXI-?dZTH}_w6+2wCdtFolQXMsPDxZO?du`$A9h}iybcY>j&-i(e~$!L z>t$!PXuSE8H2$pl;5U<9(M;^5J&l20p_-6KEZ)=(?!^4?dKGThXzGSbb)S zESjhhm+W|fpH9k<2{J3;F-hb^f4btt9{-BsbT;bf9Zf;GQ-Kd@3!Me4di-mpYRSv! z$a0z07`m&)&7*hE@aJxg-KjHO^XbcULVz-4cK9kHlLu179h5E^q_bnI3_J5}u;%MO zZxI2^evLd}OtT(f^~88r`EuK-PyF4b4u%n(~%m#$7Elv+j?Pf1MO9)=F3cu zR)lmu_f9hP!G?aI0f>f%dz1|9MCrF} z71;7O4?R@G1>C>nrRpp2sUTPw0`)v##~@kxqOTtrGW?WN*I)kj5%+iWUZv@B*W$A#i?r8I;&&_Qn8k;=+}?*;HYn~)3{unB=JnB8 z&g>3;w4s{ifJ*pPpB;x|YZVwzv*32jd4 zLW8ZMjl+*LkeonQ??~!olK7gs`M#xNrMM4U+Q)&P^^8z_2LTkta626gzUd-~+h~m8 z@fmfYsgm5h8|g#@v`03F;g2$BuQEZi!S*Mor=|$rfXnYq`RM|9opM_+=|3|zB&8d$ zq#F|rqWOjI<(Z<1TRCT8WO{SF>-2|h5cK3aVa*(|;>*Q5<-v_>yQKE0QcU;54+ zhL&50oD7jX zoypKVv`2RoY+9onuO=kJ*!W?En6DtFDdLgACT7L$GMa6~@Th=>3@x@-V3JqKJ*{rX z|3g}J=`gZ(qwulMVMPu+>oIZa&x0Z_q0Ew&5A-SIG=HXaXSRHKBGaiW?x2CLcC9@5 zW9x7{>>0P9duv8rhdo1<7{v&v9#C=1WL!Oon4!qLh=r{L_W!Q!;B6)pxyF zB*=0mj8fmGG)ooINI)G?(M=lQP4%R!G9AF|p}XZh&uLkp|2_Sqv+*YVXp@Vg4C))< zITb7yo}~sEqfUnN1+B^Mub;MDXB_CodJf6y00s3iCrtYNTNi+8WmtFPNQqndSSWKW z%6QY$`O(f~RywHd!|hHa!#^@r*|)6E2QNeuPU0H6mct(syoOEEcwrUn*MOYuZ5C|^ zsn+aHx^kq*{*QBLp7nBebVE9TIhb!t6au}^Ken7CzhwS$gBy4=Vzv+U9=cPhd^)R= zRU}9h8rn)VZtuq#35Qz!*eX5EIlOw}S~~&1x~&&e&H*qR;ML4{f(;X-V!Xo5<%IH= z1`x7aOiX<{Kjg2%uD&8x_~uMy<(m^??siyPWlaLMwHW3+xE| z9u=HrAfEd1xlWE`sy5SZ_I1`vdYkK2aIEn`joJCrpXn^Mu2a-|C`DYWjC%OTu@{|` zia^+v!^q?EWg4JsHfN_tp!y_$>Mam`9Z!oh;#5~AwYdM+Yq2A4H`-9*YIA*qrmTr5 z;TwhhwbI0qGz*uhy&8Cq?oMIdn5nwvO}5=M2*C#rkoXV4D*+o4z<`38q?nvohBKcfe`xdEYo+%MlFqd4_LE27w^~X%bS_x|InQSiKv#5$P%B6Glhhw3u%;jLpP$O5r>w7; zos29&gK1_ax~9NvB>J)b00T}kqqLbO8k&c62tpKk$Pkw#j zgjyz)@qj}48CKOOx5aBF9C&bUFG1|?)X5KgfazSN14!y8N9lyOQV=0y-4s-)+l7He zK>y=G3lZCP{yQR0U0(33uV)cCn}+1IKrEnO43X>2tj{cG=2&Yb_h1eF-D`fr36v~G zf9(0SbvUz90_TH&v%j~URJ~eJ3bnn`16E&Sgld2C1Dkkr=wxOsFhB+UeXJ1q9QC%s zp@kz4%9HKx(*b-6f2JZ|HgFCxixed!E z$2i)Az*gU^0d7#wARfO0mP8a_&}{iILdE!6H|UpmijlcVqYliX1M>0PIz=8rseU8O zCiiER>U<1yeeO~aeuga8PeULCB#9t-P zLt5^U+nc{9OZve(ri82eRq|59X z`ee2>PkT$sqytv9^CXC4FQeyU0lto2gRN8z86HBjOyEi{TU;?o*3EcIgR>EMY&;03 z?%XjX(Dc%ul(7A0a zwHarmH(p`#h0BV7-u7aF6UKg~>Kf2vmj{Qa16)q99F`zb7;!KN95U|53{QHRui2Xd zbUtQht+20>UYX}#fxL>dJO2rp06M-DD@yX`P!2bT9@l1vB z{I*@>Wi%WLOWqqnnN8&eLBGzAH(2fc$ z^I(BBvT;|yDK}&zx`6dA4}c|UPU9;QqXBOBy~kmYU%QvvA!*-!(V)WGa(EuHcBU4> z^Fv(W7Is^KJDkKYrFspH)#^%7a z|0EwHY(cDE^bZZzjEJX;%0s)?^5C;A!RzJ0y9gbX1zKEce)}Sd)T+2}a}@RbhuBvK z4}&0lwwuFNA65DNasi{#*Rr=$AF%UgUr^$`Lv4~}uvO7Q?a(e~|Coa*aA`P}lX*W4 zIQ|xRClku4@{TkxLN?i(FDS!yewbaeiuumrfQnt_2)$j$vYttS7{9Ji?_eiQ&HBxL zE@?6N#L*|yD5Z4G8^xdKY82>{D_^nl;xb@WCcoFxWJJ#=K=JWcp7%8WHP>nqW`QK) zlcN~u;Q+_Gv^OrWFZ#KLk=0k&WenLGv=)k7J+_;YWwHNImKN6z8-=WdIZ*QryG0R=8R40 z0qTaM2a8H%LV3!78<4g7W*=5j6!f3j?8d0~wj)=!&qLM7ogb$MqF>iOXtoktNNU-N zQg1GTUN(%Ps6fq$Yv`TBAIcLw=+URNc^_?SzsEU9`HzjF3 z7NhNS01k5*w3&EQK198$Un-?yNYYP7} zr1a4y_mHn z0|e7x?KdhBj9Gu2S2NS!YIhYjIkc$r0PV>JIk&FT`UeoP_)Jrfrk5MY`Pl}Jb=w1K zIV*-MCe!?YBa9DX-1C=FCKUuwW948LeBTkr#W@1MQ7)AVqm3Glxf>3^%bt5`ynQh& zkZ_H160|s<=?PSL%LQFnM5KmDiz*_$nU?iX9W|q3LbICPL{P33@F)@{bxs1n6K`-I)aeRAx{yiW@NL7||9M;aBdsje-( zrof^*Y)X(s!1*egoINfo5D){uF6S}G&G_da18nu|nsef@Rg=?$7TOkRvE;Ljvimm_ zK^-mMLAQLu4nY|j3L)PvH>KK5LMd?q3pR7Cimj<}ph?m~zw6M=8z)Ae@+?0mwRn~O1$Xv~n2&)1myF_~alLzl z@`2u+x@OdUa4Ukv(!i=-^g2_Zqww~PnLqCtO^jAD^H~z{@m;u$JE@O9dt4fB?7@2&LGpD_tCUVXw_xs+@>VCf&sBW;j^oz%6IkY>cG2ApDw{5?9~19jbJ z1Vm6iz}53SSf)O7zx(~^4k-l==_j5^Z>2Zvg8fB0{S8a=KFYjIf~M8p97y=h55(_Q zXHIQ&7|Q%WGQ^2{Z3G&8zKa)u>MP&JtSCfcB4)w;<=X9x{n3PmXUBn$1{!wk#DVvB z(5=)mce8g8IbF{~DNclTYuS#|4iS_ZU&iuD^b8-lL;~^8`J$!*fK1#cfL=grp=zhW zgn{WhDo(}5q{0`t%SBOgH;*LiF=YF%3cH${T_wj**m4D1(4eZenvjeuIoa)s-LWuH zMjSZui-h^VTJyFbfx*j|z^e^-?v9DC&6 zp^}_1Kvi;x@ffv9u2e{@(elQ@FL?6ZzdGuCkcY+JLod(LPi%Oo;SUcA{Lq0G0$R;A zkeWN(Dld)>kw?`y0k>Jzwq_xCV<*55y-9UuN4vl|oL)!Tdi55QwjNhBrq+;Txq0*{ zR`6?gK0Uw}9LpCGw|=uBv$FmpX=f}9HB%lTx3$An?Q`Quz~S8in$E9}i)>raQ0UPa z6dKl6A@dEK!o(i$Oo$?%g9BAH31la;4$Agmcd&n!fg_OfgU&#L2Kew8@4Q&=-B6xv ztsTRwC!aCBABC{oXT(ulZbSN7?^6U>Ti-py{tfUuO+`IUFQ5`&Y5O9)Wg%B!l;0fV za47~CE%G{QhOhNyg`_&>*o*S~{vPw9q=h4WD_;c_M={CIAE(Wh{+LxOt~5EfH&~g$ zYgJ3D9xSB&iP)ZOu!Lvh`3SnLAKnkw`QUy}vN^S}YSBm%R8OMZmGkh~dS;S|HQyj0 zax-QT3B6|oa)K_mz*a56Oh);$tY2`jLuxDiPWL3SqtJKjiXI8=&{qt}9|X$-k^LPS z()}Mta=B9H2ACE#7oN+EhOX$45h#=?6yiS48`&x^YxjIfqh$$8IR^7}lW1QHTw#*G z`LX%de9zrCR$&B@S9*2Fpr^&W8?PrZxX}H)g!g;rhABapkY<`k>dx$IL0TLzDmP zLSD75t(}42qage5C{q@jhJsAtv({x2ceTFk ztLElC5_z4jjkl@UFTzI5BGXE5P~({PO_v$6nBgHQ%-B#YgV!7hOq0)$Fn7Z^1tdk? zLC`r6;ia~IZSS^DDWT1|6m-A%xBY)qeN{k|Tle=Kxe9)158-_xg$};O9%}FQw1`l-C26Yq8h>fiGxxk)KErZJG%ccpc#{1W8A{%HSU6sqjBAaD}~ z&o~%RdF1b|<*ELWjCJKXwynd&4{@8|(Y+Fr8bZ2G*Nt|Z{uCn|V#M6!LEQy=EP3EP zL4eUw6qC8rQ2z||{j8sP%h{dag?8)>D}ZY{g#Q#z#C?y8ABc-TD^sGT%j>tAI`GTX zdJ^ttQYU2o>TB>1zd+wRkjV#ME?2H^?vGx;ThzkWWQC9XW!*AYA9yo%IYa;H;!}m7LoPOlrROIO_5Op)ZTYkQ zb%fe+B;bB$%`$%SVG!`z8>U|9hamF>`C6iaDkncPcSrZzAKLkVdV^2r9rhr=0D(TE z>Z3F+J0GF#jBm?r_P@mbNOrm!rN2{LQ4NASzj@>VCOX+3x4}@_8sakx#g^(qmC%pW|9NsaE28=WI#1FHLd86YtNvWmx{!5;d7V%;_N+Rr(SK-oG z%<*&9q1`G8HdGm?)=&0)gettsL{Y?m3J*J-FUV2nq*t~(Tre9fc2|vMKz)JJdn7br zS%H}k&-960O1VIyk~%(3l(A3f2bdUP>JK3(el&lPVcS6Vu&Rq<##N;k?>aAMh^z?gGFLxQCo zSXk(8za!47r(gCy=Ae!jR?}$H(v#P@@(Vf%Yw6YOv})eaflj|<^~VB%7B8ZsP{eLJ zito0hLRM8q1n85KpWz0&=j?Wf%)8Upz*8>0Ni4gvjNAA+h>Od`lM(+ncxMIuR$nd} z5M^0%xXA(gGq=e!>77t86a2%^ooj%uGfSGAz8|>fNo`6+Ei%!G0(;?|{}K>A-;Q=W zza>+{S64!F`6@|)4DJ(BmuW_Zxv>qi?v<218|h~>JP|&iMlQtrN>D>nNfYO29#H`w z6OsOOtR_9$T$)oRs|jN9>soI5>#%4<9@OD5BdxUbH06om>i4Wjq(tll(40K@SixCS#X`q~57mAEYUHH( zJ@ijMf&(gslhH~vZtI)=QVPDta-eEjj`5Ialv?#BN2%g!(mjp?Bm-=CR?0xG8FpUu z@M(}_XBp6x5-@3=Zgn_(E-*A%2g!Ju=9kPwrwEMBwipvlNPLU7On5+cDdJJngd%vW z9#L_4c;b)~WyYwD0}h*WC@bDql}r-r^uhI$$)klf_)q<*LJdpmM>1g|@hntf?L76F z<;CB?-Nu!e08UbF#)9nad~x1$-^(o*R{d>lZQEC!g-aMv{rxNXu&02fz3Hje`9eWZ z{H9VVnAn;=D6g?Z1XT#;T1KRq$n|sc58LudP=glbT$AxbQT|>93^b-?5*yVFFOxqk z-u0a@s44bZQ$8?&Pa>?c*Q)+Rr)B5joHM@vpLCBcvimh90k7D=*^eR1>p>q8``tul z*BHTmQ7K9lkrGauVWAwXg|2(@J-Q`j+uW1`D@ys74|(rRg#Pm z(@0uZ2f8+$6D&J)#PqKSWc-UZ&c3^dmG-x%pW02V%`qMoFeU}xMHLlAheXtB2>ZJR zL_y>gP-&KF%@k7*-#wFv=L0eKNluO@EV#yUt2oJzieIL$*^Z9zEDW7 zQ8lkA5pvoxfJHWNb81?Dp#PzuUuo%_T|4=u)z{?=D1E=hXvJA#3Dc`GAMB5$Kd9%! zxP;)gc^mI7g&zc8$$;rq`+(c?JMRX1{S_sj4BOt!vO9Pnreyvgg*AWyswRG!4UV69 zAB^NLJ2y$_;ITI(F34UM@`<71p^0u8IMLeZPW79#_cp%FwrM**`fP z14|V{9rFX@c9J_i)<*|3{EexE(`;2T!>>Bb^AAgxS%zuQg8Y72VXA!eOQW_FDJB;^ zBKxok&ud!PxF*5b4Q%uX4TfivzneP3-qBJh(XM+DHd4MgFNiD- zZEhvf73fObhor|1D@4Hsump{eswmo{-^2uTjh=6lQ)Zc%ZVrD$8*q~fXW^6wmj(Bf z@_L6wlLw6-=TUlANZMQNzR5k(av9DlA}WBj`t?j+xhdCJNPcp=(8hh|+s7YG8*;J9 zA}HQ>flh}!+%Uo7anM`D`|-NRzC`z$xK1SG2k#lxv9j%>MQ!CHhy^HO*2|S zoRwZ{D~SUH!o~-l1`K*HI+tSib_IGcV`D$-`Yu?05lI$n1#>c(saf^|;&y*dv`Nuq zG9L&p5`SBqUq)2GEsDtF&|W$^TpAwF#MC;{XhZNa@CinjR(gMv(C=H429U4GNVHwa z5n?-Ita_ymV(>Ds(E*ceyo^yGdWmiwzUr@5DOvHp2A)%{EtDr_H92Bp>8B&w?$Q^i z9yClz|M0^x%dm7IG!J9vs_{|`2JfqG4l|%4sg}(J!4*bn#i`2}yCq;m z$gWtQPI>*%?_{{d@1UMPs9`HNfOI_>3b^DHa&Y3<-*AXC1W*vKQe6oIRm5qrQB9u&457CpVFM+f-`>2D*)Ju1Z?!~s*&S7$NksEfFegE2X=!B3+^ZXZAp46_1G9eqn~Y3)pUxtXhrE4Ze5YU=#_>}uG#rj+`KQxX;-Kl7ej?oxp`U?aUV?Rt zUwsGg4CR0gIU2jXH+uAF#T$ZQr z?B`fzdqalX3>(!Q^1pM393Hz~&F?KM2ty5@m1&d9aFZg59{a4^pf@Ct+ z0<_kpQEi4cQmQB6(&~7|F^!?yr?Ry~D%K>Z8+Xfe)-0_CI*zPE{NjqWEMDG$&7+Du z5Vz=rzjjFaodW3<_%E6FV@r}f#pGe22$RAN-CG_OvONpWm;RyAohJusKbVT!B_B@U ze>I$Z_PAcb+_k02x3Xx8^TSrJ`-EUnur(1XMVR0dAsH#Cqp3~mCzGNR1BLFuCizE`>>nO8_*vg5fXC`T*sJ5PFCRdWD@&)TU{% z*F_H%l$_W`0~7;@jICA3x=zB#3pgs6sfsF@JM}UJ>ce$fWf^ERxtvcJTw@T=*aGuhqW5N6V93RjL#F84S_`R?yofA%O$7 zJ7e`f8+Ts_hG;|GF{Q;_dW?lz{S>x`M|!pux`Wfp`JRPx=Ub#Htzh%P(EpMBq289 zHaq$=^UgE+_200aBS));U{?inZC^+JQ1b4xLoTUD@PDbQJ1fY~{Tmp*+6Xf89$LECSBvmQD&KCqjdCz4_J%*| zXGP+!ATZKg?D;jRTQ-6ILo)SXhVFT`FE~vnj+_eIjGOBX9LVlUuKWf^%iX%)mIM2= zv>>#V-5Y#G2N4w>*?DVPAbp@HlYAZ#p24F@8X^~SPkaDR`1}H~jd^g&a)Mr6*U;gf^k9z%1PUOC;M?vwjJdLM5NFl1hV_{B$BfM}jb?h7J_`0}cu-R#ma@rEbt738KjUK@r(K)Tq+-rcxU4D7hB z*=f_nJUWyO302p25+x9(KM$n_pMN7?h3O6?F%Dm8S#>60y23WM<0qg=x_<0;4;U&- z?Me5L3ERrA&GfjdB_LEv@1ofWj_KE3B_1;_XiHrF9|<40$@h5OM0R#e0<#Poqh4`s z(s-{G8zLE6sSg14MCK6dUO=l4U-k-p9vqr5>*50Vo#rRzHr=tBEKiy+kJ4*$Lvu@n ztj;9O08VRTbVohsitkK9x8ailv2@)>HJ6TX(Ct7ovc6`Oo z>gzk{k;ivL1xy-=ERq-Sga5#zm-nX9Dp=vsMl^RknBbS$^v?E?IHk5M{dh1DRD1!L z&!?;960)DXSt&Rl$UZ3$KA2t#ZkMlpvlV@+FydRRx4FyP=4#9xY7zhl#W`KKuumqK zVlKjbmOVO!rcGlj-#n-jh`E(4ek%;r}uZA5OLV zzHl{fp9@{-cD1e&ZrBzk`GHk3qY$TvA&g6oJsaiu_aTG-ZbF&DTs*^l*T*JKL!HJ+ zrAI*q?>&aZ;yGCeczFqMd(f}2yvaALmR%H4Ja3FV{oLJQtVs~kupPQjs`yjU`DPlv zy^7Rfs^Oy^B2@iq7a00J`a)xS=4|hAVuDU9H^DXhzbv_l#{TrQvFowPmKpL1#*OGd z4VOcR@qCCRJ>S`mwRs@iz8vqH(CY_?7e3g{OPj0smLO8>CrqtC@#xe>A0JTS?(_9M z%e^gj{2|djhC=2E+|kx^*d&8lu_LTL%j)ZR{gtXIUukdqvxtVGoragR#pt@WC217C znY1|*i6(sBr0|ZLC$Jn&))`K=vbuDg`gZj*uFjh;aF3FYkMB1F(ANG7*5)ROUqc$b zpScS!uLjCJU&gBFuVd*EDEtRGzYbC;p*Mtr=eN5ahh- z-DBsY%85`v2AmmI#5_Mz6XXUB22di#mLd?ZAtG4{Qa)KPvcb3dx?TpoX=npvo=m%E zblnUsiKhBJlCqlDQBaeVhd6&8%7P#UpYVl$N`3Xso1OXcay8~DC6@xHElj+>au%M< zl+@Pg%P3<>MpOg3J8`R66qI38c(n-8N~Oi6;D*U9uagmS9ewTi-L!^Yho1jC0|Ey= zsn_nBNgwxgHH;7K_j;ciz+hr3#2j|6X;K;l8|_-DBEkc3Lp4Vc8Oqmy&jLD2{G9e# z`nBrB-K1EfJRkfM;M}*VOG`dJc!^0m(<`qX-b)bkIoMyH#q4qL-zNlQW?ZNkYNEG$@zpT!M7SN>-lk}0 zbHE18Qv-N$Y&Iy~yQ~kNhHjBy=95LrW}ElW3O)_U)~3X|yByBArE>ptt7!30S87Ls z!l>2n{Zh-^=}Q<5uO}+_sA($q{fWTxL(U0`rRztqek-< zYH_L}V&c_+Jrl~9^NZ``k}HWNp&DlO)t9iL`g1{&euY{ zA&i4Gx$qj6w(0Xp-6xpTBdnN-$6MDTPJ@1%aM($})Rdxtsez~41^I#dUYQvZ(gev;E_>Ho~kIfYhGE;vkC!|DS+Xmn~gDs0-7l9e;~vrn@d_) zNN%M7;%qjl-`PoeD=ZHp%&&HT`0k?jNoT7wUGsdS>?^W)J{Fg~l<(d1WKWA7>-NZtn zny>gTlrJ+?-xUXJm9e%5Zho?<M!D4-&-2_)bV)~!2pPxiS z=}=aXX2!hnda$IVzH}0mH90!l$1U{9B2L%LzTTvBAT92qwP zS`ktGK;_aN_eZ$OOdEvJj-HJ4K%BksKUZ!A-^CA9s$L)0YHJD)Uoc!Iz5W*(ml4HJ z>DTTrUCuaZOI?NMLH^?=+%{Kw#<3rW-+WZ(IJLtjb3yWuUL7y)UCjeSDnNBt{7~7| zt{F z9ONAIMn4YBw8g|ng3MJj?c*s^Xd7T++sjqz(}GEmPN+HV)yT0veC% zwhH>N-M{3>QyP;Fi5fsh$Ud1kQ^1NcTj0?30o9<;B-lL-F{1}e)%0Bd;Q~gv_=Z=Y zW_N@J{rn9~@j_J$Y23LbK>DF@sOUKwVR&lrT?OtoR$+<)Z})sufH2c+p;C8HQbwWT z?|re_Uyj>TUN~+SHl8XFPl2_B?1P|^}e&C#{b7HlX}GHoQ6Lw?i6iaqksqSHEzRnyZp28Qo&+wGc8yii`I zb)NZkz}zU^L2Vfg9x zp{Zwu(-n_^SuWTbt)Fg>1ug3qEmXb+UJ?wI9Wtj z-ZF%|kFezT;XZ?(qeczLA6HJqFs44iXyIqRTi9VE4os|_`vH?*WaN*Voxm%&(tE*OgERvh`mk5_kB3yj z`XUw?VQ^%FKo!cd$Q-H=qE2H~O44j8uI!?v&`)@4LV`Xr(zR$bh4>mER(q8|}k>Gw#LiVxKi@P<=YX zXEKM8WgS&7*czo?yD4IeFDu8pUzqqkwZ|SN$0+pEqS&IZSBp>+C*~z%biFuPZvMN> z+h+HM4tB8gD(Xr%;U3#5cFyRy2w|xEi%3Ptn!3LB>B8|xPZqqPw)-hzcb_@%jl1aS z(UXz_Q!*I`l3V1_Rx?_UKCkDMC)Wcep(^m{=t~#%on?VorLDtj9uYC#fp$5IQh+Z@Zepui|*oK5#m@R zuDLY14xHC!$S3BrS>x%7`q#Xcf&|`|f&*1z+!S)$JaF(cx&}!&fJM9~P+xtuHA#aw zZq^1TDpi-X@X>2D=+d>yE^DTE>>m$lw{rOEYw^4I!I+*&TJ zzbA!UPO!m*@YT{4P4#Qf9v4ObhrdTk55BH{u)}@VB3QMAO6ZIY+%rP&A2OrMI>8XF zLV6L}FRe7Y44}9`6U2#`lJ&-*L*&47w0mz^tH25-nm&F2GgMB#EBkP_hon9CTD(Q@nKbCw>>Yva{TZG zCjzH&mT@)8&&>@f$A_W&Xi+kY(Fl-P4?-wxW!Ac($8EHaa^?Ml#X+dhL^99J? zHJr`EV`EXfsmtA;NY?{}O1+D(v@@e|`s?t)mFZgH3^?nLTOcC+E1*71S!8dvwm_P9 zn8hasi~n9#6~qwdfI_xu`yKi*{#)k1Uu1&;0K>1{9314YGT47WUaz=smn_ zQxb9ff=`K}r*lK7?c*ra%(f49>%j^st@eD33q_x~zI zBIys1U8UZ-(sP3uwrRse6mNXEE?AZ!*iD)UDwI#YT=apZ?7ef21_76>DaTsEb{B-8 zIK!}VM27r+)gsFpb5~>w;o0`$Uv?gjK5a{EvFz8hAwrG6Emd46Ut?hb_c4r1SYqKR z`B!no#N-tQh4*XMZ>U+}+dD9l1oFguPnt$MusEDox5LX$2N2-I zAWXX)_>)MOxSwTb5?=vHCW zhCF<$2|~8eM6FkL(;}M!@fXxPux^9eSvY8uY8-@G6PtC4cq46c9ygb4F(K9(PI3(! zuO5&AM`c*6navgOCZ+zO#UGn)mO7_!5JbVa&l@p^_RnWZ6rHqS4Z~H%*($+>F64^t zIMT$=Ob`eWocSb#Um&-a&IC+86t#FL)_#>am<9;!*x>7_^}*^J30c`uh`)+QEz=HYFNa#(;_rvk`vNCy2STOD3gphx8z~|Kgv?tH-Z_U{ zA@oGr)MZvz*L4fGuGfkX5k9O*-=6r#LH^Q?FAh7dCR+Uem2Hv=IEXbU58q|edwD2Yd;KsJG#GOkQ<|R8J5i`1w%A4%P(s^ zo|)MHse~!v&*ri8xJ6}6gwDRr1%ZB#_>!8~I_LTs=)*9X!{Q_MU?uw+5iJDVulNeZ z;a>;#2E8;#n$$APS(Gy6fiGPsORVh**Pojm zug{&$_`(?7BloZ50NKQiax^4@=>+04EfXV_Zw8wUC|ky874p>=s|^a%)%R~iA6m0LoMT& z*%vo|Zb{*Li8xFBT5O^_%bZr`?mHWsAAMF;<-SzSH|1vpH!*j&W!^U_E(kHfDHhpq z*J{tuZ-MZt2|3t%%YgdToS+NbJz!UZ9{>u?-}U+0xr;SIQ3YI#o@u8NFdg;@PIo!L z!~PF}vlDvg`mSTl^9=9BzE2BviDkJBiQ#OkkCF1qPBIyM9t-Yy)_AiRYr>Lb(zixZTRfvhgi58vZ#5UGU!oGMs~~M zQxo-0u7!(0%SSL{Ae-aTbLF@F1ef26RlqA~4h}V`C=iYMl+~8gHEmz5_vUaj1l!zx7;Fg#9@vwUUADPCB3FC`AB5e5Xa3am9?U`Y z>(fel)QHmv$;$QsC$K)+&GS?@_k};hi0?Uqtf}Q?V8>iq7R?*2C_`=8k(@_pbe^ zqtOd+MP9(>n^2Y0RP&p&I*nDWZokK#r>&z_F9pF6`LXFwHN!sx@f?Snv*7o{{LmR~h`{8gRVh-Q;;Daj8uu56KP1A=OHdx0IR~ z!b9g&jh~AFu=CSUh$mLINf>y)ZK2Z}XB@uDNw> za7&}sTMu#y1H?0{_Y9FioUXT2S^^^mZIViFUyg_t1nw@JiXHyhax0zekf7*qHNTl^ z6jd&%VZJ?-@%<+=8izX#O;3OM3vUEL zH6L`J9~8+hC~$2_krify`Fy<=O^J_F2q7hNdUdzp_Jl{(%axy{8_-u7@MgI(=@A6C zw*@raFTy1)31dc&hG05nzU>>etTY*T;vbdXTtdSkwIw?%@6#9@GLIDcMib2t z;U4R6J>SWdq^*qmekZ6OAt# z#c7*I7*L&~%Oe*{iWguRXvC}beW2BLee2)bhxe!%I2}Bfz{HY z5Q~rF{L4!De|)?NRdzFPl#X0%SnX1uhL+sQO+7`xrOeX431e!{X-bCIFi{XqMcj~{ zS*L=2$|8>!3x6`aArKuD@bn+r__Qww=nssr;2a=*M24vipOu3?-klbZfLXJ`p_y@b z&8pMYrMLF$&GVRZ?$akPM15-=ON*lpPW@m1{B%2MD-I%21K@giDP!$Ua1>RM;0Ms$ znfDg{G>=k;Bp%bgYnSZrDx>x3Not$_sug?V`d_D_=I}kgx%Su=^!SlkLp zAvD~{>l-D5N$l1a>-EPZXTvYAI$M)Z#(jDOg(72>+Fm6oDv;@mHZPm+I)-j1j&t^R zn0J!~bXU02_Sw81;K5H$^#bPJ#U!41Wn5!Ag3KnA3q$^5yRZ1{`>;#Pk{_-K<`DlBq$q$66!3h7`U)SLkOW+{sKLNp+H;UlzL8vb!32A@wnJ+1uNLF5BtpVz)wH zuaCH;uJbcX?Ec1m76s>o*etaB)BWk@Vx^CE2=T*o=T+(fm%Jm}`&CY*XP*gizbBgMf@X-_X%|j!7JtO5obK%{ z$@*HV+;%e)V-w%}<11kbyM(EruS3_e;>vJD#DU5&@Ko>qbn`M4 za5DEzE!NF=j&F3{xXn1>FolaxXJTWfx)sfNH>p8j2s7q$(pVyOtLh?LTZXq@L(G}o z^@)y67<5_cmpaFW3y(BJx0sx_wU2g+ z2a6OeTvhmRP1GF`m#O+sidF;NQs>v8n&z}!`w7t%Md}OVa@|JK zcENGQ`)8Dv|F|fQ%KeAFYrQYyJ>Kmu+-1Hd2=ne}cXi#lpBjeRuqd)3r0|b*2PQxXNMe??STwO;np(1%y{8{4{#{{7( zNXhJGKinL2R*`uihO@ol_Z;BSf(#WBmZ?tzs-cc z#xmG=C*JY8shwHIkoCW>`S6mVN0~o*4z0;$Dx<-RM;)6-n>+3Ce(*w=gAy3w{Pmwk z%h9d$-f~epBxs)QXF&l7DfMnHEB7d%fp@5(IgN?Xi&gy$le=M0AG<<{4BtDYQPz^l zpJV4bgTOa|^l*SX!thq(%05|GS3+iPRt^Z_M^cm`(GwdT?mL}qi~zka17poMlkmOz zso7%1dzIwztV{VP_H#T-MZA=EuLt(rgS9@=o6@l2s@sHtjm=?#!MJ+XjiL!Zhj;Pf z{r-_3>l$SVZW&QmeFK08b9TIaweYYeM zr1ks6mE9=0=;87AJoK3$IGDf{=-)lZcP|@|ib-f4I>AC(wOo0WW-jMl!uj_o&a@=i zi7zt5S{`U(UU+4>^Yr^q{1<{S6u_lhhvR2c74o>Xd{Wy&4iJrBymj+((Ls*^d2mXH zDDtri1su(awisCODn{3P*N&CM_Ij7+a;}2#wBk>e2z1V+C)!k;&_>IEUD)pOG!_JJ zGN-xLjt*()@>&%`-o=Fkkllq4um0+^{>ecXq10mL@92}+I);~iV&T))bYSCXqk{?B z#W~>uOYhPllPSjUerv5wh^}nacGH6}hR!v*& zRx@#55SQ+WA!o~nLGi!Dr1@djB#V_6t|dBEHKp;8K0i3@vqAan?57erN~+da0sPZ* z$wN&%GKfXP#P!>}X+@I3B6Nq7pW#A!XnA0CghN4@5PI2M5)?j@*v%bYDYhrEq&A>T z=Fg*H%M(a{Sr0)Yd8t9;E3t13o_!awUrvCP>78q@|DK6Ckqm^JH!7dNE=L}&wHyg` zxMleg5aJ}ki2D`{z@yEMNmUCI#ec3EAHt7D&6TtS&(XAQYIc zp~J|3)UjE9f^1lR_v%~2#wR0WApdQ0^qhTy9Px_UN=bYfoN7ehcBl4Y27U~bNS&^T zbkm^!YV|N}R+3yA^(}8!F3xpGp9CG zZF_hcJPs2C`WLS~xUGEyrkO_E6utHKO3a?@1SR;Gth2?1otF7LNJzI{2LWD$mwOC$2$f#DmoC z4e6Et;PsLa`GOa5euXVsU}*yEdX)O0L3ylaFx|MWf9-hKp4s!LvBa$KNwLCz&i?SA ze&FJvRV>Sxmk0M#!^Jl>cWYKWLL_I98%MBhC2rS&cZF?UFJgt~T}_Bjw&=P*RWwx* z(+f#&-O=i}a)KN2Aun<3kbQU)+l2}{JluHDyLatTeHceINl=vDNx%l8S|5J1|Nlu$ zuV`A-lQeqei--F;h;9vJ{y90(I%gGOD+J3uPQFg6mA6vl3$-$OI>kCXcSn4Xw>D_? zDMA60Ic}!GU6sc4+(G=9f(>pAp)TVuI{#Eqtxn5AtWoK1V(C+8oU-z@X?f$3?)>T* zdGCuyVF#KJP%}W)Xz4JPpIUc$L9=R90x8c)h ztp3=LqsGe^080S(sZC9pmIBngL&4V+l7^5T%AWjH)!@B@uS?8|bK~J~a_zk$Bm^4q z40VTDbfe&?HU72`0hp}t*Ka7?fvS#v{WV;`J#}G^cg5*go%_c=>`UDD3tEYDgDQnk z=R7<^i3HTJMc9xcg;kgl0sG!dGSt>&3^}MBy-ZD}h=TK0u{|0NvGk0GN<4mx=}mXG zFRkt_{MJr`;02Ci)7GY~dl4;^Q+#%yM2J!)KAc;-EabIv;h9H=|4rOSl7AbAPYRP6 z%ZPfk!8FbK@4L*R_=YNwFJ(D%v=WNVJxk_5;c7F@dW|+YTG2{rVPRnonqVODIq;^& z`QA~$+lXOQC>&H&s7l}#c9W3c2hG|73M(aIt>wVljN0Y&B)pg z=kgUTayD27plH`OH1iyT++mm@RfC(gni+-7A@255h`Z)4u`z`!A)fRt@ zu;ng)nLQVUQNW*^g#Wd?Ygw}15M$G{A5JK>pl9Poj_L$VP}txlr=@?h2cDov)7U4R zpxvJm4CQ!}7*-fLbz2k3Nd6ig*1G95AD;WFs-t5#*ZJ>aaOTP1Rdrpj_(Zd0VVUo* zchJ9<PbFd_j5S4>UQ+Io=GjoD13=uj=ON$Zw+fk>?7tMjBD36=` z@U)++vz+>B#RcG%UcT~sPf^IHfRo^x(wL2|yT-57oN&8BJV|eImc)^pfrfIpf^eqfvvyEtgojnw2~GzrSo`lPKSBeSMXL#_I=i z9&+MBEJX6XMCjD7m$GlWC;J~qi6c*cYs|EUIr&z8`xw;F<8)e{0S9%@!y;(sA6bj!r~Uw(BEMA9zi-eu@zcRh6t4Y4ZG+{c7mJpHRiI-N8RP z`l7yqIb8LYN9Gw}@$qF9872VQQP6i&` zj`}?JV~lx>I>BjRowAf!t-h@;_1a1n=QH10k!41J=xpo=gb7lKJfa-h5Dhzw2e^`uTW=`bDab5Ip36?t6`lU}vy(e`P` zzrBzAbff0!zfl5aGgRg7))Q`OWR!cC9bd}~lu4Psqi!8}Bz|c>ux2LS>3cOaW&pZy zqs%!dKURfzSy9Y3Nt%l=nt!hSFKC2W1{X5adp`fwU(|sqANt?FSN@qj%37(%G z`KG8B)m>#OAfWMTK`ubi2sxVbD}DCuOyS1U*Zqf$)X;sL=$z8q=d3p{7w%K+q9unX zBgysGJVH~I4nc_6@9M5D=F$&gux+SYjVzK9GxZ(qQ)tiwZ|$1FU#Ed)cuv`l0FH(V zyDT^L)v?hUm>?2RA(EhhF94C0NJv7+nir~?4JMZ4FGDSh4N1yQkbcl~lx5U40-F6W z0UwW_Y~AJvfKDw;hCpU}041ofW0--2#eegKZz&Oy~xBe z24uH>htLVj=)`v;m-mv|^o4Ej3&H-v{s(*&zKAD;zz|PR<;(!yhiG3%}wf4q`bt2rS6%eS!uqI zp-(O5##4tZ`KoDCOWrc(Q|Xv{aPo#M=lNE3M3-ZK;6g4t z7;Ex%a(W2WtH@@lLuMK6&p3s*jITm(Ai2{mX|}Z;3Cp(pE}7mW7;o}6?Baf>?~`Z( zUPh&Q@QBuTK9;;8un>929iv>c3_a2OezuP~-XwCMt~qrE6M*O*T>B=jwbo%fgeJf6 zyPw-|AgE9gIg1as1Uo)rjnN6T&t1we0^B6h+wG>iufcxzmlWAQO%E*bdoqG0OQ~8* z^IT%z^jXq@+oe5!aX6?|sh?{WOIG_pP2DI`_x(cc$i)q7e6&gvR~Fb`I&_f@H90M) z<{*MMUapajDz}@y**`x-T-c`?HM{0R8JwbC;8`h|M}D2!Yig$s0#anZp$%#q-QYX1 zHbjAnJpEI0FWWW)E4?bAKs-oTK@IClH`IzjS30LeM=*RAjBlc)l7LK%3$QLbhAIqC zn{91A^24P%%7={SU&8uXR1_CEt_>xM!(AOSj(5_RLls?8#xob7aK|{gfHcKh3e7G9 z=upWnb9Xi(POoj~(PvoLiA|6Or|@LVKV7=`3#O)+czl2@5A0_m6Mrxqri4w-cQW4f zf?*Eaz1;>Bg#{!u`w_8{bI(l+KZPTh+$ewmqc&NgpD|P^sZ|ZPvB7_Cm1*H(V?I8f zVlR<_-*mQSaeD0V1h2e}@y!PFB2_i#yVs>nnttKJL|{MSF_>#$c@AcN`Zf>$q>CZO z2ZaXH_FH!SU3v)~3|&%bNiSetu%NDaj%>Oz&9nxe7P2QoMd}X~?ofBvfG_3S< zLWQ`&=zJ3GHNy(K+|?^=+|Qv*hV9T-whbpM`i(i%9zFc124FgK!>=^19AHTFS1vs} z(a#XuY*w-V^2V3iat;);0*qG<6UX4moRROpF&LS9p7Go~f*cx4vOl#4f4DP*Ecm4t@?JAyqcuB@MFVU!P=0_FIH5?b!RoYZ-XpSIMJ1t*?PsN^uy0-?RWYl5{; zIs?vvmCX$#pZp88vq9g3)|r}18aUl9ET+!S?dF7|wsZ}#ngdCqsI9_h*UDgTE(V3k z-Rs*I@s4?|oZcq7`lP~3O@=HnTg%JKw94I{9@;6=dPIkRqTNV#9INiq%hugq3))NFHs{!epYo;Pl$;cN+1t=BI5jY}Su+AZP`n zhW?j8H>@_UyTw1888$Z1HE1cgHJu21yVJXbGB*0nzS3#SYm3Js1)L%jskb?4^6i)R^O=o zlWNfY6mQkYpepe*chv0gRRzK!YEhC%(%QM5AqjY|M#L7@@yLe=7}o`|XV@8h)n{0I z!S%^|FNd#X+8%qwqMi0%4C{?$Fy!rJsg<9XnLJm?r{?u!hyg5kC>A8E=aVQdUS4nG zM}U-{@JGwwsS`lgK0Hz3hy3^eOs<=QH{=z4f8qEm2poCALD2A=4MVHfCV!r3EYxAb z4SMI2OsW}1{_^8X4gn>QeDi5yN8@y>U)Rq8UO|aN?4lPTS0kCsidc^08uUe$9a^Up z_2JdP0AZM8-)`%*o(0cWgt=^P{=MRJjEr+91I~QA&%d2h^KPM5?Q>^}0&V(qV6AtD8TJ%fBQ3nr<(*+SwHr2`H9U)3lLG+M93jo0?pezR~{GtOs%9Xvk8}`)3)i_IT)~PR;s1At{A!>xgt(O{c-Al_PF08f;L)J zP)bZ86L(@sEmo2(m)y8l-}=-bwXPulJ-&)9n-L2@(HglbcsLwI1Y93F)PDCC7hSb( z=kj&Oa`~-68$W;%Jn(3gsT3H|)s#>3(Tk{?`T<9Q2Zig+FpbWT2v7ldHg+oYxywQZx^ep?nc)rmFt5xEUv#w)~SqMHgOggALVC3j=ly}SCL2~NiJR)~#3%!7WsUmv_PyrA(+6&^#JF?cCSqMLMTR7v zT4gjiT8DczjN3fCF8Q4LSKpV>B0+2Fk-&abyfJuMj45Edn2$>=h#ZxlinU3M7)B!<+q$rNzx+>`<4M36k zv>BwE%$)y!EM0dX)!+L+_u5+|BQtyNk-f6XE}KwR5gBFO2-$mPl$jl}#TA)lm5?1O zlB{g^{*KT0cm3y&^FHtMKIb{l^Lk#->j87e%iU-x)OcjLJCON!CG@HNGlx5r@>o!SPH@Vc-(9PglX6woJiWk@hBV*AQSgp zs>BDEU55|14$H#KaTUW)QpO|7c4cF{nE#HXge1v3vDpNlu{U`30ZD)^#jC8;-2LIA zWb=1A^ph2tha0AY27}NZHtPZk^d4jzJr=V%4G4z~?`Sx%6gE&FZ^9&a~CL%0!aL;<7>4^y5%UhdX4!nw#cW6L66EXnYiDWSY z?0?}ORPDv7gcj~`95$TbV}`9Zwem#dOr~4&i=F64VN(pV@=93JZZFvOkb|wEB+b&8 zgoH%BvHBfVW~4!@hNR9){9C6^LkyCFwEnE<@6;k>MFSc6H!z?=W!>zX1-_HZgJTpJ6W!DB@xDhL}*K)nIQTo8oFKlea8BgbO8!ltTjnWyZ z*`zjX+YqLs0cnKGiw|UBgF&>phkeDm*-nb*XSN*QR4Qn>I2TN(I(*0 zd%1bBJ~`U6$glXJ$a2YUdV}b$+mv7Sbzo;GahmySWodNzVvsgi{L}a zRY0(=_V>m$Z^oBrERk%<45*J@4g4*-PIkkvfPWUTV5P| z6joMWS~gej^#1jBIGTk58Srv!^PapG4f?Dd0*}f9At?J^?RD(%A}5N#fDcT(Yku-} zwid{Uw_jReslrguQh}Ziv_eU3(|N|yo|o>ot4^loaCw^LOwLfD!=qqdY#f3m;6=IG zN&r7B#Tr&bAo^xUoP2HaGpzcj9~@>k+3vY?D)4`vaCi<^tkCPH`CCd`iBYP&px13R z_pApVPkvwHz{KLyZ7(@oMF%`5YcYg5K{bx2U2uQ@QoF%Q8c&W*ub)`-L{7uracXwa z*dZxb4n!mYJW|OMS6Y3VL)~vwS6`anwCf7Zxn+hx1uDr01dOw=bnU1>fRvtad&2L%OYVq*RZbL;7YchJHnz^A{?cgJTyrJ+JLwDBEksUR9?DnXvIpFCrZHQ@eoaJbL(W|6( zaq4(V&CM1=>)LI7J3KB|Y{fB{Lg$Z^6>S(qDvWW2^(S$7xF~{kcv2>?51;Oa(lx1z z_AeFWRhYaB{Ua#*IY@o_@{rmf8is@{R1gz#M=iB`8J2 zWhTmpN20*Cz1C7vc}H^P%+Na!NL(sLP`UNqWy15he@66QN!iee4(@Y48#7KS*`WEFrRY#4@5<@PK~^9W!j!DJ*t{vhooud>tH6 zlE7SLF~(qb{fX=c1kxw)l87s~i(iQ0Dc8);6aEzLvx1{I-Qp|B4Ch8?=Ah(5{Si zUj|{X;QbZw9gw!Z(Yf_eqei|}6~5?m9cP*G@_sr!x+WR&U)Q>`j_2*lD_{zJfiDe{ z$Sr}12){EwUo_%xU1QME6Oc1+$UlMelGAKr8ec&Go{(tc-iMyU9xuw@eR%Y)C@0=j zGN{mrM7g48qw68O6Q3U8q~Rto5n)0PSuO#z71t;e&pC?{4>k&CW%ylIL%#N;&#SLJ z$S*U4^!Wv$TpZv^>o*~?9uzcO0~?{0W@aE);WVJbfM&>_dd*$dVLi@OH&ITJk#Ghd zbr-dw*@QgX2qXea`l;!m**wS%Wn|^M3Uy3y8qB-pbUi*eTo%$*irTJdn_tsw>0#vRsOTEK0W++d!`Vy2pU0E+;>D6Hd9oPScXx@IV zwvN4ha&h{e#=z9dvJ4)|FM2|#UlcWgOBo5X0c_>QRWM_&sV(rNMtAe+W(S%-EHA8o2=b%SE+I;4F>g^ugK%2j4~lRT%TeS9om=TR+}t>^XGnNMo{z&zQX z?@UqzhnoCy4kDp;@yK2aw<8vS&Zy1P%zQSH{5K~Q1p1L6<8&Ajk=AKlBw)NmV zQCtEWuExGzz~66r`$3K+BMwjMxb3&BWWljGzF*51eqS*GS{1uHctX@ANc(8$apw(J zjR_KT5%h={;1rd34{s;|RFQsZIO`FMrwfK4N>jDO1MyN%XoIP_rkH4HrXlfjP@q&* zvW{L1>wn%t3TEMot*xzdpqtLTUb|lDNW5j?YxAOsCru0dcj9+xWxQlu=t2vQ`elv? zWGcq?JBvBfTc&y^Zzc~Z}SM&?X#wHC81#pG71c{==xiH&m#im;K1wWQY z&ufR)d%WiT28%n=5=@XX^2NjezW(gXGg^HFBH;=VW*m;X?8`{bjZ(#3m%zi7 zu?+e5ju4YbY`WX+kfKEmoVTjBrM)>7z%P%MxqLE}394%^+1?8Wx?~;Yzt1RN@!g8t z^bnMcVZ0SeauY_7}}V!IL}G1XZS@si8Y zfcWyhB5X6nIjLB`Mv#CXhY9cL^M3A$f0`#}Zq%HERBpU5KarGY7aoq9Sc{{uz(?L$ zn_Jv#@v{mvWx$XbLzTZmr;JitErR_u=*38^hr$u|9q`Hfr!ovqF%%luZU?heywT`q zCAdqs?^_lqSlI}Zl*C;@{WrydJWB@aT%|5bAq4~J^(*7*mvAsSCi$w{o(fU`y?o6K zuwbdWFYSyc;RYNQ;R44yux-Tm{_&?%9x`z$UOE>185$_2NGeV|2XV~u4-$nqHkQ3~dC2MvglK)=%Po6sOEikj-lfN(n*h(BMnc{c+q!>~ zsi0*c^li#g*Ku#^N4P&4GL=nG0CDDYID}C&Q{XX7r@Sn4#$=3?`DlpeCBgO@$eeAz0AcCQ=vdb%v|5nQ><|&X4S?+37b{t?~C8b z0OaC~PK^45LVzY8aEw1~74D?&_FbNzZI7it0omsyFGr0O-&mmct$W1?W;EStj* zub;NY!xK15CsK=0PNK|y2APzZ@HxkUe@`^&EKH5m3}`}aE)X?(c3oa5QI_L}!{yue zE3;DLD*`KDC-ZH*m6U8cUjOrXg8Tj(8-_?ggmSR5nE1Kc@oGO!50NyyV45wQjeI$d zOGz%x=)q{Cg#?VuU6{^|oq!G%FLj51EEN=}Z%8|gJO0RryjK<2vW?@_<9FW!M>3WsxH>~supziBa8za0_YZq+n+ z{ldy7gBfhT@>JI9?QyC58cC;FnVYxqxH6~IDTnB2V? z__9kFy*{{bYS}C;uSqIl<2sM3H}20MDbtC{9+KK?gQD*}T{po)PAYAHB5)sDw;Msg zTe*l!J0(EK5&T~6qC8F=@>Z0)F8=&vjWPD6S{N>xvhhkZrru8q1GME7X`Fh5giP?A`DhN!~yysjG(Pv@jr_P%Mg;JWG- z9GEio_4$w41~EL)jirQMyq=EH@AvgF>7Sj|^t0o*SL-EUC48tK^zpP@Bf^trZICZ1 z6wM?F)RT~2*t0%@%DbUjHQngp#Sa@wam?$8q*B)o>%VsXg>zM%?2~ea0x<;erS*l!&F`i3J>*iK->&J4=x;Kqu-@xjg*Bb#U6F;IwAfw1|i+Oeq-;ZJdAH zzqS!DwH`bFdm}oYe$mZ=1TAs_NuYa9l%I|3s|Lxym zyRU`Curej_jZ#y!sqwublaavd&W-rD#Kr3HfW}2UBEUMQdxCuVm>x|b{cATdCG;r7 zsh!Cj>tM>+S83V=-WW~b*o);__xH&>im>IN6Z+2#b3RoN9=>YVS^Djd)cu+KoGext z?OyvU4Fu$*3GHzmoRJI9aj~@+|7j|)eNNhTRFiNnwJC+>>aML*3E}^?-Pp5J-`k6xd3H-a*lotV+y}4Ay{bJBu;ER`Sr;@KTW2CE z=AH3;;0Ob?ePMX9MJMq|Ju~#kfulPCo;znLjFqweccwg%KbywBnIX9uvy~M}2oMbZ zGuA2iw6sD3{A~fr4;D7;vdcu5GHbHTn}A5Yok*MF*{KU5;o{<1E8o)bSRm1oR;pR3 z>ppiP5sF0}85(yUo=htSAZY2m{`hO1&dEz7*};2vr7tTyenNS~j?Zv`wM3~Q(ZU6G zOR=9ck5<*xXD4u!ouaR;$&3$0wJXAh)UPVzIU<$`lFddJ&(pn5G+*PRiU=w(+vexH ze<{%ey2hVMs{25S1mY!^QCW5E+k073cjd=ZdX@fn-Mlg~M!eMK1JH;1{5fChL;<{A z4vVkf^y-gFJ~Wcx3zJ?}QwnzjGO`@gnTm9`|3x*cnq%xj@_lHWo)8GWREfO^`IAH%p!i#c+;J`h*dKY6;9*WD~qN8kqIXfbW~^} z2zjj{37+7fWl~Xg$k6rhr)Hr64IIe{KiWiuQnu~N--q3knG7yW8w@D5Fj+Nd30461 zTJT;HgAQMtkN(bn!kuj7*vAw4+G|ck9O9+&h&UMu+-J?INDe)yY|XA+4GjvB(9yX4 zCQBT3xp#&rs?n>p?0#G@Y>EAAvz+(FY?*Visi@ok>|L}YSQ|N?3(F)1jgzZ?y5)X1 zZdgA7JeyDm5EY5qHXkG|=Rm#@1R66KZ!Cr44d6-N2PI;OmV+%?Du`Y-i+;ThZU5AR zD^Ef;v~8_9EFf9*zX?-8^GOA&GPHcT{2NDP*O;7O8YzPeJ)#KSq_hP-J?W{f|4 zoY1;Q#0UbjbebMJ+&;1ju=FR%6`_TTc90}ITYI*J+4g(oP2Ojg!r56oy%YD^(X1i4 zBD>xk@7&~8>!dNW0*EA7{*kobZso0uxvxm|_q#EIRSw)C08-0Wc$;AN(+e0P`d!?% z6x$^br9D@%n@D-obI#eV1kH%RaG?dsyot@|DUlSLwl zP|L<7v>B7v%fZW;fDgUuN19QS*th}X=q|#zk}KqHONuJj0Qm&#cH$?#&`7X#mG?Z8 z8tP2%CVC*eif8;JTTSn8u68u}Wt-oft@@IZa6VzrL-~L_d9HHwzeDI-+7~a^75Miu z%?+#=v>hQ%4FB!ptrzFfV4W6hvkvkpAwvTUpJCb!nZE(dx0QKI-nsz`uP3Y=JR;xt?>~F&**St|EG5oqgl@G|WeY-AoX=L_E zyg|gFq6wbTb5tZg4NxlyaK_vS{hj`Cj?`0MKO!{f)$YgM_Mmm$cqS3A%l7<$G}RRE zt)ihB9_bT2`&j7lJ;i7)_?LaevX8PJMJ=nYD>3qJn(Zh3pF+FLkL=~T&oEr znJPPypx?Yi+H0-mzkA!;)>vOSb$2%Zo6SOL9M_{hrbQEr3_}O9ts^gv0dtfchN%#` zd$=N|_21-p{@AwOHp7$0Hf|^qtx!~+d(pkzXGvfzx$exQdZzksHzlXPbS359#DeoT zq5X*z4lrFrYA>^G#hlXJd8H)dvfxL~{gX;bw^H8pJp9L>2du@6c&3UA&L%^3s1wf? ziIxbHXj&GJSP_gZDq_k_uV2bKcW5>C*LKpTvv>t)2{S@R%P6Je44c@OzdNAjmV z?Ms`Jo7g3hg1?Gu=C)b>^~5n6sGg_95LMb5V~@Yx}B^*h_<`9#!P|y#<<)#fXjakEI@ z5y}lih1`EG(r1BTU%$Tpy?K^uU5)9N1z`jf(5sHN0+xdMd#j_tJI`iF6H$H(`7){OZpo^pn>y#nQIWlU6Bt6*9c(=A`U!pcx1oI!sz_<+lK0T z78Oyb@x_I3_ux`vz41HUp;vWe|HW-ou1LDAFCA~*RCPUayESfERsp=&MD3qc@T6=9 zh`>~sm+7F3GVlBcghE_cw~NYkG6upsc7Ma+Q$&VVuNOwtV7;DkY8!JRjEkNu@aYKX zDpq}+H zvWT104a=rC9Vd%y%I^w_g`evYKVF`ySghu-+HQ|9Vz0zZ{KjA6iR}(8bc|!S0yz@& zwOvo#Gt1vG)ZB`jgtzL4o`2Bn8YtJHHe0b7p|3?Db0aI1NHaGM|Lg*2 z9boXFNJ{_xfr7F_TlDE};#SoG`%#n;H_@M;5W{;ZZ*u#Wz}wh|20mWFov*(Ayk|y; zd8bM$agnjnhKj+(R?E8|e#nlFj7&g(=&GxlNo||HN8+*(0#+9(p<{^RI#sUOzmWtB zS-2-DJjoAVPS{_1suUNV*%b>lO>vs`CsoS^s266~WU~$GO+i&v2hS9rDnIH1L4Te_ z`xxRavduoKMsgkZ11qffZf2Wgd*>SJIFbGU?~}QMAxDL%?}NQNnp-hWVe>ErLqv7t z_<^M_n&VCAu|E;OPoZ3MDvjB8H#hRheBf7A+9w6mF6UPr$R1s!5vh3HEiA7!28Bp2 z9M7g82)Q=2J&?D1n{`yFl~FN_%7Y8qsE2Hhzd5GQ3Hxj`CVs0mX|wVoS2SR(REL;L zPAW<13H!Mqnfj<>#^~oS#%jYV@O*LtSMBKqsGiq0dF{dYwei+Bt430$n+Rp5Ykx04 zo-pSFFi~lrC}DgGF+KEjlGy{IpwN}fgsqt7vpKD2=$4dVtTBcH9P0Fhy>^%&O652G zUY4ieYcsydcn!;@1`RU4ji9hKtMAVD8--EtubQx(QS5Y)A_gA)pmt3{0RQ8GJ(;Yq zoxS9H6|6_Et3!rI`IQda7gd0xX3E#;LyB$u_sDw;-*a>GAP@nsuornvwx>TLQ$$gO zs7Ml+{3#a$!+$vc?K?^;S-I~%GEO`^l+TUXM!lI}#Ib_dHNg)i==Y4VbTcU7VW>bO@6tn}8_(AzSM|rSP-! znS`q7@=#aZW&|IkE4#Hj4|C)afxM8sGA{=j8pa z^~uAK=ga%eYt5F$S#^Z67pWWl0r0H7Ee;X%%K{g{cl|!lTD_YOmI#tc zQT((-Fvs-kUY}2T&<^dJzbt*J9L3}N6ifPim7VuitFL8Nu6)XAqgt%fgORY9Se`u( zoN=L7KNmet%kj6LJ%8Wnzt#Vu?cewjq4haU3nmcJ6cME;ks7Cxpxh>;){<~E8H^x4 zyDEm=cLkm?WLWL0)PIh7Ot)UoLL@pbcX>_s>T}H~(yr}S4j(>NOj;IJ9c!g8=UmI- z;SR25PQm%wzzG7xi^r)Lgt}!?mO^tYfYUz~`?=n}w_UnmyYz$XJ-!pqK|1Ang>-AB z4|`olPPfnPVjfa;8^TnEL`+FHUgMP&tA>UKDy&IVg*Tm4)ruNU=p~diUMiH zgId`Tc1yr#!Wu>3Z+}g5)`r+t4fWkyuc_my7ltmQXdeH)L&FURoMXIp_q(}_uwPBN zUn34`fHvmI^>l+{8GK3wm{OB)?RYi_L>}~&5ZhyHa7Fyy;E^p=>lW`T>GNj#c&78E zvB<|kpIA&Vk4OPTT_ik{7p}xW(F}9i7m3^DRXv}z^SHU*w{J)erIGjt#|J8+tvFH< z8Dr0g`aX&~Qp#gtGR(e8>%GMR1_y0*`O5Xr-5-jV{l&hDJL^{u()=-t4qbi5g}Q)> zk_K?W%xpcQP{C|#Y0cj!@8I^&v+a}AZPSXsMvwEnix8&kY_gOvQ@A2h3+LnvuErRp z&;-yI$&sTJBl;F7Rw}bJ!AHDUzkZ|sal>ch}z~f&_4GRTWj&Z6!qK}g9gT%z`UmgBRI^e%F^(juk z)#9P+90<4NJM&RDMjs3|U+t^?^MEHhJ|J1%@jTS!;$T!Xtfg2gcAl~?Z2#?xG-wDomF4RTTkn~K>cK{*qr0q==%-syGw_TfpATa%uSdkdFFGyiB83;w3 zJs-=X?;D&kssVI6f}}vTVn}g65EppH-nsf6E75yB!dFkx$VsR3c)m&T>O%Xrd+eN> zAQc~s&0qwT%#4aDu-c~Wxhn0hP`d7=!i=l(iqT3xALiti?$gH+q{TpqXsHS!Fw=>` z1#xXEs`OwBr^Mm{Wl$(%Hexs1-^TsSFuv*0T_- zLnO3+_sb;^xTxkoYg3gR0L>%!^=0z2B^Hf&)+5bp^h=7Ownl|Cbu(7K(ijgJY)X%h zZ}T$U(s8(erLVQxHM>Or2suzne2V${p!-Y122a;u@6Dx1)cklS}ZzBGO&~8l$S{6@E1y)Iy zWpM>oP}hw1ghu$Y?Kso1iy*cN5`)WgW=Tb2G<-<_AFwxNJ0b`$6ex){fA}NYAmE;l z&3q?QP2RlW-rlG^7JET3P!}UMgnD!srF4>jQ~VCBi62PFcJ`aT#G-t|d&?QoWUaD{>oyuta86Ghc(a;pf? zuCHg4m43H!b9axG?KG53_qr=)cd~tG$m}1AI@QyVVIu-XcTHiU;3T+bJYtDuNoB(m zkz{+%p}S$Ly6{2#3LP3|{@!mgR!*~%v5mcfCdVVTS9tL8xM{z^mpc@wcad}xOsSGF z_JA31AnY$ID2B}fjmJT3j0)jPb{ZH2I{f9dca8C=+1z^kH~J3uc$aLaScxZqlp6Zf zA2{f1GAnq2hYzO}Nc$A5g2(Bqv&+f`0$7u9LBMtr+(b{8SnOp|Le{$^5FB}8t-for zQhl%Xpx0D+qv|rlE3ykR)HUsDuYU7K4i@9o;uTCavpQ}&2@87$+EP`ax?j40ymAWe ze*_e*fS=Fxk;>Z?MaXU?^c$~DA9KFZ3EVfdKLKTSbgxP0D|7Srbr$>&5Xz(y7cs_6 z*crP5=;bo%&#X`s9V}+D;)}MfQv=(|!M_fZ;S?#~U|BQ}VWFsSHX7iYCnCwcez&!9 z1am~HbiT@8hXjx=cfsu;KKw0rxakp9=2lfg8krYQ1Ii5)(&d7rAxojk9CC5Ls&(-;K-n{fSHE=XHtG zI!Qw*;31*A!U2g73FJ`3fPn3pl=*kYuaAukh2_Z!)<)cqBXi!3{cuaZ96T1u$wrrx zHRBfX`DabV=bO*u+RAs-11V{M=VsPfHXO+d4L1e8gaetMy4x&0z$9+^QYh|3+Xw%nUP=#1g##sMY!zXK)C}) z)v!>D<4hkQLl-^ap~h-jDSdmkS9fvm-(4Bs+R8)gBSWR_Ol=oPk0OOrrQ%HPf|gV8 zQXY3(XWj7UK>`2WGGT&=Y zaXO<6^C9XNTseHNXDxk3s@d|{`G4r`8^Po!Ir&_6dghO7 z!rtzKq}Xzggt}ii!0UF9ZV-@K?EPDa_n)NE+Wq7|varL2gso)HXMIAhNPphxU>P#u9XYPeWG}$ml6@ zRcd3|b+i#M!AOL$nr_4MBIIpy;w8gIBt>mx8<{Lym(A1d2{hlDY#0^#-R3zR-4G-Q(FSDK^y7%y=p3Xx*;vsYX|FL=*f28M_iWi2 zl0>4^2v&-hSnaEr(TCCjwi!BtK&E(TUW#|L#F{)d-(kI=+2&O-0m}L!|LxZJ23J-O z8XOD$+*0P?l-c9A?Si+m(texPGE)Mt*Mr?Z0x^##9T*^EV}$i6^I<6br)Zry?^Uxs zjSmY=T#ofjHyQo}3&`og6aUHvYm@xQ1jz^UWLrg!U;b>B{JPHV?N zH|gBTj`{W#-BePT8&=#6ntVhIw$Qz#0b?A;F``g=oI~J&Y>#W9W5?%t!Ijq4zav8t zG`k@Xv|m3{g)SS?ejp_ZY(xLipXD>f7lzr_iv{p(}=W-v`J_F7x@pOD1>`)7tkQh zG3pNY?oa`mIr6pNk7?}FM6HOIyZkH6x#m8ZENoq#BmZkLEKT#t*50kLgCUOO839!3 zU`yS|$h1w-tsU3+=KM|I4%+UGNeQ_&ild`DBnsFUlbXLCW2D?FGtve2Bw+65Zcq#{ zKkCxYT^sMA)PvlziNh*(GsK3|jNzjnS?rdC(r&JI`>y>;VnujSme%ZMizi-+ki$5o zzP{DMw?ZT4C?%ZLG0zF;v|Y#vHD~@XBRcM~gT-WW7PnyopA&xCf zlzN=-19HpQdR1a?pR-)BAF3u(T)`-8)q8~{lNZd#{mwRW8cby?D9$0`6ann|IXIm^ zL(vs9kQpaAZ5(lq0V^uH;Xc(J)-}A;%^+#;;7;BZ!9n^v1#GIwCGauobM}m@uUzg; zsR}_bahct@b|8v?5VD>%79baOk&jVf8VNl6UzYRbOC$Q8_e!hYQ@mFOU0A<>hWVO> z`NA8j*sf(&_?;~UztcQPrh-}YkT{NsOhJ$>9itlg@x(ZafEeyDgFzDeOe!fA?hDaLHLOVXX>o+o}57W6GNR%M>kD8<%=^m2* z>-(+R%G9zDfsyXb6XQvIX+!22VHEovk-rQk$fZ-2m$)meic{x_X8))GBWG2WfS2Xy zjY|ugm-HhWKCR;0tv}#72`tObxV3Uw+2em7&cikO%TR=riQ@~;g*NTgn2jx43i`V4 z+>Z-?e&YhoNIjyY1weRU&j>4-Psz;BZ*}SeuP-gTZTr&vYH_LbG=ZR3?4FfLS&X10 zb|{tYz-|?Cq7aXs!<(qy+&voR%K~bS2|qv5OyX@1Q%r#W)=z+5ccFIa9AH8o>PzZJ|IN7#f(r^H>tUy@+Zyk{#Ub5)30laz#yBiqQix}X({m`YbRjWyDevViQoF4hd zAtfVxSo6)dJV)ER(|fHEBRQO4{~9i&82!U>;-5aB3jW^fZtAgn<+N*%F6-3&KAc0; z_EKOR5*HO962AbTLo&!?LUdNQK>-%kuIb;CF_RS1={M!O8L@k{scdA-k&RTg+Q}Gn z8^VChNk{MlE_x%vOy~TeW@+F2w*A0bo`>liH+wvQB1EWQ5niByw^gJWj(dvTJa$W4 zhXn@!^*8;mb#wiPRy$zmY5KE&ovt;i>9T_83Q;69{I9I4%rT>*$@gyy5`o?53Ytjq zwQ0Ul^Qg_N<_qN=n}eIgXr0;dH#6)gwV%heW|bJ08wFJ0NX2@A?So$S&#H_|5ssOq zycW`N(($hOo}+jEVZlW>R$Vdt!CI!VJzB{x&PCN~gqI?etS5y{_q)VLWH40IB-55e zF$RU6>Eb#*aKxon&@zaxRY~oFsU_1V08LRA+?n^QH&wDKZdW07H$gXDKG=Fj`)ufd zKZRL&Z|?#bowi)Hf(g@&%pS|E6&@D zpWpkCf{q(C{_fK1FFKvd++-buEcjD*28n2BRe!E$PqV~d>=7B(%1o&vQ<;MDC<5iL zSUHzP3QSMv+7=nCcu2+9N7f12cf{TCC7^YnLhgvL`W6O38DdefaRRp+`Us zH`gCOnXZ{ykX2A{_%Rk5wF}a3wsDLz2~f~~be^>{SH;W6bGN&`<;2cdlIUgBg|cS` zX%n7SDDWVmqd1a#|7I}Q;NVEtKQ!$W2c7tF_G%i36G_>?(6xI8dE1L=9iElLUuP(~`| zbp2t$RsHZ_Op0=v5EeiPXV??60q&&1{;4y_F&=Ht7uR}~DaPkEbIbNBZ=WjxIQzU9 z|NRI%t5xww5e5b~+w(lEQX!LUK#N*=aqmm}3>QE(JA{S;T<0R9y~$;j*~O*Wh)YFK zs@a0p9HU0X+G#|$m9biaw3qs`yD=hpqq;&i=fC9!)mM(!)|}hCBYxd2s~&W+_WV+Q zcOX-W0}-?_`1b)d6h&qw_-8y4ivdN>Y2l%`$wkZ3O0j4bz)U&Sp3F1)^@_l3u8*Hv zVfKz+nJwPgCtoOGBK{rgpay-20S<6o$zMJ?5L-sP78nsr5ho#UxZ17SY}*RuO%4ZN zDgg$(gajk<0ZKjQ2jLqp6Xg@TXn;IxQ(#`lt}DY8?0sn|WU)ciz-m>)Al<)am*k8L z%0%+3A(}PUGgpr8Y``<+A536<`ueS(5 zu%nI`ZKVh0Knal1s2&Og!<=wQ^_o zW+eaemMbAl{qLOMygc#Wi~Yn@x*l0*(47{!c`HL^Vgd(!GA+`xRRhZuaGt57mm`?L zwjY_!x#$E1h#dK|3CTSikV+}SNp`cw(CYeeWxP#(LEF&AjC1sjaa{X>|J)yvVU3Z| zKh`DZ5 zI`5=jKn)*?R;}ulu0miL^T^-0+M~eyY%VMfTLgW!izf^ZS9Va2Bq|*EbiSK$5W~A! zIy9AHIfs+#wsv2}s1C!20HovZq5&=iY&Rn#fq2$@2P>oGx)s$g1Q_WK+Dltn-JMBL zqC)Dq5HjUS{SYw<)%U2Nl`Vjf3S`=P5OpJ59$VV)P3)$~=H>mEcU7~JZm;m`xWvpc zlo=z#_NnP3Or!yVJ4u8xfnhEf8}(S0*FrsBao)#W0XwtmEI~2$g!udzmU96rVHp2T zFI0{UI*H#fp0O5}T;y-llif)Rx;1XvGYX^2-JwEuI$2Y|h%%3r6cncZl=~wITUb4x z>&oYB)|Q0ee_m|LC6YIMp^%Eh8I9^zyu-`Q{15%eH0}f!mFRGF5J%c&Q+f|e+)oo4 zv%~_(FEzDmM10UI2X7L5nC>veMY~IBp=42B^$?Rq}_aCy&7w;DpiR=6K_@~*}Q$+;D*P$B>u2gXeC!=0;Wag&? zOy}d%6o8!b#iG}@$0=Ahu0Gj#SZ6PaSL^9}Y`vo1k&uj(p`1o~ffIkfTHDYMV;U`K zUmiyZOg4NumVc$H%?aw(Z5Mq#Jd<>op`}n$4I{rkSh1qHpt-NB6!8afIQtDhUY^MG z=jf$50-ikrRI>5boEFCsMHLwls`zz9ZV7?%VNBamwHN7*U2TKvX%`O6?ja0y9=_Cq zP-y&3iKg?mhA;dR+n_E%XH?tG>48STuNE7zBXIB@6RCu`V9MaGKw;nU!G?Og zB`q@%;u0H?7kGoWgc=AAR?HoVq|RiT+|1CciKBRG8lgw;{&V%I$yOCSRBYVvsT}m5 zaTYp_7NKU}?Z(LFyJMg14i1Vw!UrV_`}^^dEx$`=29Z8~IuOp+z??L0NLBBVOam`v zxc7P7w5vZhJ-&ZXzo^x|j2Pn(%Wz+|De?ALx&I!Ad(oC1pf1&g#w`hB$)Dhz@#JIq z;>psgx~`ud9u$?cfH+y7J*8@GCZ7tEoAr&?_2-JUp~EejH*H2MTVhK`U zf5FndzNen*A(|^qyZmNgf`{syjgM*{+db8{=Sc>XYg*XYL0%)(p8%bna#PWzsefLb z_s^gk{@I5lKWz?I8|9#ehgLDtjrl>VFi%JklSIV+?b?t@4d?QGeb_7Y83$@Nh-$JvvPEdn4!a<8TD)-&o6H#SZK}XkNH;1b>Ws zI5AaaF1tnFFNUypRMVL2(70*$ZA|n^g%~XIflPSTkFizxHedITh;Dq+l)qoSt1j+5 z>U*=({*KyCw;EgEk723xM;QS04Gh;VW?$Rh(u`Kz7grzc;I|GTVA5X6B~32SMJ_>t z4HG@pe%H~THxt;Y716JkVENa2P1nZTRO7osG!F#@Tv9UJP!AY`nfnnMvCH1E_D2_9 z%*1}-q~%iR-Ulp;&E^B1p`M{G_KkAe0dhLe#`@)MDrWg_CY`=d87Mx%D>jRP1E<&i z1=@)3YGz$%ctNQ3U`6xUedp7DnG~AfW4Xc(7Lf3E;gg?ruW4Ud>Aq9En~QFrQ%BJQ z@sj?d5^?c-+Z1A;iibvAY$>fllFHb`$+MYTVvSeC?&Q61d17;%|7S=`xqySnDn%PK z3q#c#1YpDqzmN1zq2FcMvCEa1VS}aaZT%UPU%jP0~|inEX+d>C)q7SgxBO)wOBQNS~@18%iEGxa%_U)!(*c#vd-X5qD)(xRI+d z(;Fo=5gkMchlJD3Gb0ZakIH@^=K8p%k_^Kvf@2J;L%3gnb)(sKOlJX0r(Pe(e%^8U zYf^7MN&1t?H6VYt&h^uawMzNJ{DPl*ZJnpxHq^X5A&hnI3# z&!IO)D#|}%ydSiO|9Fb+J{KVX*NA%av6jA6r_{)(x{pDILKq`G#ph0Ps51k|w2SJq zk6=M4!NlTA|GlvB)t>jA!h<5|8Lp~S$Ve%FAI0>%!pWmW?-t0sC4dF~;Zkzk7NB&> zuH;YYfhpD6w}%3$MqC_YR9tzo_4C7NlA$RV!Nofx)us8wg*?in-{jvl3Kii-A zd^X`(D>j6sAg6-;^sIi;w){Ks6b}}cmoXhlb7GtJSNt4F!tE&jeo*FGbA!e+_|efX zIG@g81aaUSP`Qw&AnWfhsc_)o^A8&T=)(T)IZR6@!?`)963P5?s(BpLztpS@XYZ9M%A zrOw~$6=Y9C%C!Q8cpB244HS?_<;qOW2Bb42Qo<1Ems7+ogK$2Cs1~u7*izY{O-Lnv z<)GN9jiOT6FuW-?jhY@pCPIZ1Wa?wCgd1a8>RAljufo@$h>-vvr<&6RY%_9n4@`Br zZJOr}OmBY`r_TLyH_I*a(h-XY2Ov-Tex>X2tj{&KL*GbrKTXl> ze(&*O)L>-$&-(lOpUHjqs<&VA%JY8uI_t&EtX{=LBUcGm4*tBdR{z|JC*L8qh?&hQ zzx!Ku*gA6ne;CW}EGK^WT2nvsy3U5)t;!dl3JN})U=c9*K=IyS6nDrp5HGPQ z6_9sPEsZn&J>VoJ)ssSGAyJN;#t11N1234kzx}yuJ!*WbU3|T_ZJ2q z7m2Eva5?^SNiBaE=9*7=#)5>4h%fj(cHURBpE|VNrlqvJj!(dZF;7J?SrA0#?R+%# z2nzq-9Ld8EGH*L2yH16DlY*>3Zrbvks zg`Ui(kw1STW(NI(rYvrykG#g5C_Uy*1+r7)1phx`{_l|f%d=LZmgCwoYu3*uL)W~O zviEPD?YT;0V{h&fe~Mbu^S~-yd}`wJ`(Ssp_&|bY#COPP;2x}6D4|8==9ay%JC}t{ zG5j&U4chxXay`Y(`TIojM-fNoz~VnOgGa0Tj$3m(y`Ei|m_m|aH&@tq3~C6V)Kuyu zkR3<2KNS?3EI)m_R({L-oyB1a?wk5Vt~2-IOaDjHRR%QKf9-*&gbIR)gdozbbPpwD z)RbN?q8p}&UId%u87l4jbAd3 z19IK@;@CazH|rTC7yL~0@gPN5H(ae6xIaFLf}xux>^DIss{p|LVG=NRV3)4ug~#3g z3V&DXK6@l`yqL9-y1Bhm_oh+3HOi1#V<|If>}d3o^BRz_|9m!Et{UaWu=UXRBBJ$r zWM8OfqJ6Q&q9CjZEc$M!YdjqtkO};|SGSyuREV_Xx(7);tRKpHX#D&z>1~LCn(|bL zD%$lWAIw?hZSUq#{`+U9j8_+ooU`L8S+>GRQ#7HX?T7S;YrcJejKQUc=2Uukz+3*l zoj?u=y8_U0Sh6lZ;ycVr`>0F#ezMXa&&HAD6X(d?Fn0AqSCB;0k&o3F>;Fdls~+b= z@3d1{WSmm*aipiw3f^rSn~(J3V|2RjW0<4SY?@}L4&?tHx*p-?2^lsT| z6!@s235|Z${F%Mu-okcQfFRbT%zbt-!$hz-;yXQU0-UG~D$rwG)LH1H{B6PP?!16y zN{bh`6@mlEpA_&;9lpigGs)@by*5<-tWlJB>&|`X{1e@`b6~MlXztHz@nRAD$8J_Y zHtkYwWDB_uj-ZS=_{cykR(EXmBnQmqlLw}&dHheJSiB6JOf;aq{s4fQ6U2NAIA#nZ z+|30BS;fo@H%}7#JOmF#)H`i6NFFp2jZ58HoyASB{<~W*#{<)6A0QfjPqxP|m&OeB zIL!|qkG{yC^R3tPs_+VB4OJ+by#kbA1HD-y7VZiRk~gZ_x0rCPv1*4GRukt6+Ksw& z0qhDD`~B(I>X<87&VOh4#!$aA%sQrA%Pz?Xsa?Xa*AtLZ*X`CVl9)eJutjFxSow1m zF%dA0|9CAdk_haTF%alpJ z+6ldXs2f+-BMyQPpBdhM*=o_3I2t>O!b|*nrXTS)PFbAP*ZA7L(9XHTh*ww4&@KuO z?|Id-yEGLl?s_mWsbM(nQn5wo1ckL;0d8Kn-X``be{yYT@7WY)d;I$6RWpB&+D}H& zK`z{~KV0V5MdAIVD$d)b(`I0>~o)()_5{ z0;}@w)U`@4HJB$E%R9g{p|_w(`_ zOIc27`eJyh1l1Lfc0W*j{F0D#s&@4e4-Pb=veg9JfY_i7@{3WJTJ z(}^;sT$5taS&Y-cj*t{QeGefLAd`>$^+uOy`ddJzX2t#}Pf@U(<5>$&cEnbi#+A4N zQ2FM{2NeDqK%jRi%~xu0nJDi+NLK4a_Hr_H4C|!=StFU5yk28HlY9i=l^_h>M%tW zqty`AD@pyY4AZ^C^L6u5c*+i`u=a`k(W!%=I0v8#IuhXFj~q-ePR%~`vhV*fk;;?b z=tJ`XVlouVvo8p-VGQGmqTcL6x)FdO{FS{qZ#Qgmw}dZZ9Nb`QeMKQT$%y(5Ed3&{ zXWH)J1wqpV&(+sgbS=!|6L7oNjuLH3IGlQ2JnE-TOfV@10o`aWK@njmMKza}s7wi7 z0o+QFD{nOl^6%d%TSk|K)RmEs(VQKU&SLQDPPKenHud|d2?Z{aIb)=^gn;K5q2HU< zc!bu=>+qJ$+^bjaUB1Ys%n z0(D&eSc(x2)Ziv3%y!q{TqbR#5B+9ZXhfQ5!BiInN=D!VB>=yYRMuCK*y=jjWU&y5 z)V;zz+ir7dXJfu)32737aKz$Y}=-W?`}L+jpvk)B@}Tj z$(W4Z&-Nd$s+qzA;I2ywhW9r?w+Srbz{{joY{<;Ft;aD1(i&j$3)EI|@(#&bvTV=h zFUXDIkQ`A}$lF;FU#>wZ|NR8$bm#*dEP(5bDP@T7Q9=#|85~A!VtU0ymfii_FGQdk zhw5dr-M!Jc9l{!s|2VMp+&ub#Ba~)wbN~|mVD{%Iv6SaN0iZT9lBA@RdT4*jNz`>^ ze{{nFFo^+a##sfG{XQQTJ-eSzo`$E5qtbe9&W_b{<9DL>&Jc8J9=Q&l5HI2 zN3+uOglGNd#_zy^!uT?`nWzGY-%2OT@e%cJ^^fL0YOH7AV!&}*vI$*jkAo!kq>m-- za=UH{+x_R1;-byzlEDbrb+68k=?D+ypZ~kFe`0sedvuF;)rLG1-(Kzfe(o+88Lq0Q zdJ03U-})V;-fCqV!nH31ggFbU-T`V8Fh;4OLqNu?LDjdJRohFyM%K&fahGWtu1kb- z=>)2=pPlyem_;O$_OIsRkepvqe5%lC z6Efbj8?eqJ&v@_Db`ZPF1I7j$`u+%>4<@N|cHDvOW4G7v~8YJJbclzX~4-!$oXXu_HnuiD=gt zlE11XjHU0y50CC1M}6PpHUYI9eAUyU(>=7!qDwJd$zv(4pV(>5aw6p4JDQ~@T#wXy z>hY~y_U$nB5Mt1eLRQqFq4S&r_*P(mQII$sEQ$y9qV>fdFH%8JWdE;)?B5h7WrL0_LTYm}KT^T~{1dEheSlt0Mm zf3RKwTB*6L7gr^G7~+8VHcRJQ5L)OSQUANGU_I0(svpb8NglBibMwn< zm$(%?$NPVfE!llkWpUhd;3=?0V7aOaiXqWU9_kf3$7F|~%_zruHAc-kjSU90^OJ;t z(oGWlQl`W_kyFcmBdeP-mB*9h|F#4B$a4R=1q@U6^qvp`XBqX|WUwi24t0bmL&) z_!CyBXL)Ihx?kPyrBG{#&|c%F$Z9@?>bqw&^>zr+ZXLPR*#t&fU^aK!og5*V`O1FP;;ofpKLcB&2OW9(K-ONj7KG!y9H(#hD{l11 z&x|Vrt&T1w*rE)E|JaH`wB4b`auq~*Dnmx?9I35hi(Lkj5XM`S3Z2XsQnP>p=>dEp z2D4jV0fLU(tuXvU^+pdyYVC^+2xQmu}?is*NLkz@|71xds}AifZO5TwPy~r6je%R!Hop`O+nVPQ_j9jXUy0UoY& zB1lmlo*-{+wDFB*l%alWQzj}KCdseVpJ?z7_Y8=PvAs)m2-iPS7iv_pAJ7_4EOxDs zvJFzI);|{(adcDc(CXemmDTwLu9ouG2A78KlDkX6u6~2F8N;FW z7d|q#54;NFQ?8y`t;deAp1}t*0cDjWsHi=#IfloI?7zPC_k+*U{`fCL9afL4xwy;- zij`2?91Vy$9YO%zvX;Zu*I~9!ooR@cxG$%FSTd?@nxq6YYy|LoB;8kB9iuKm6YGQ5 zvM0pxOHuSwWKUqhVQA+2$&nhFLalwSlFRJ$jh}8j&nkuO!#MtaPO`2g=QHY{#bT36 z@m|?`D*Bc3_#e0O2aVNYih6t(%qFxod;$2Xmp#5T9kvW{tGavf6I@65$iNO#Nhp+d z%d)Ud3rt94$+jFCk|#O!)n?9>Co__ssQK!Wux37ejS3mi_9yYcyashZPHRF(_V=T) zc^}+E)e0kM!;@i(dW$I0Vth{yYk0Vl$z8eC>0#XxSx@X+xkCph1IJ{2ee)Pv(4#cq z>RPf=G*eW#nqO@8;LlGs@k?Z`VvdYRUt3KM+cfstn@sUO;flr26Y>XDm%sbyCy$7h8)xvYsOb&K`*h9#NWCG%SnGR&6@la{ zfD^mTF$6+vtpJ#D&h?f6Y$o0gdf{p!ZG;&>SVk^O(mt@;XM*Q3P#CJEesn`&H7A=6 zjQ+lUVPBVJ_y#&(%-^9SC}Lk`TA@q4ld zq7@i7fkhd3Fe>r-VH4u&BDZ+8>S%Nl=U>a(ZXQSIv698{T5y^h^nno64{S~w{L1OV znhsj>AKm?{Lgq;0m78aF{|tL>e&;nM#VRUgclxvP-$So~C$y(Gd1Xerrn~D7?43n4 zbPQ5$_TvlNRo}$FC23Z*)>c<9_nf8gDp z(y=yu&sHvA^Kh!(`(h#c-=&BF8@Q}(S{VPfJhoVPO$BXpw1ni?EPh9P_c#23{I;p` zq}Vl0tS4Qhb4KZ%h(qY_3VwO3WmqnN5E{JmqG;JybDbvlzzhj#mmj+R-@ zeCh7)bL3sC>m{`rS;Bg&gpL?9z>gm`Zb1|7DGgXB3i&ea>K_AYe)}{?SNSmk%g6iHmoM%7ZTbZn##}AC3WhKM*p!@s#Dnw7xC2^Eexct)%)_KG7C|%Nu_R!6$@| z*V%#UvxU_xlRbcU_dJ=xRf}ONxK3VYXS{AGPk+?tv@0b%ffuALH!-WY)f$z-iY%v| z8||6R$sCh^_HTvn9u>ikgxKe}7JeKEK|rn2ex-DGi8Zw%4qMhA4wx0RO3z0va&4{K zE%4OpUB&iLj1#~rz#5BCP~M1vl6LIYVmq$y_t$pbo> zP-${X-*u9WWJFIkM#tsq-*4W#Vgko!_?n|={|hDe1cfh#_}1!FYa~>1$+ax_KwfBQ z*&$N;31*92t(q$XSc6#oE`w~+BEllTwDI_%t<>s38M1dZBReo*y*K46<2;dTkXFE^ z?gTfv$z9~R`*jFtSB-}GsBn^E>DzdV{@-tgy;yyJv+*Q9`(P`B1S80_8_BL9i9c0d z&?vA8`W{1Gqx{4SIjNCkB4Y~)*y;Dqe#KX@9s28I31N{0`09|80FpsgMdbi!MDFm!Zjc`XI^@`s@B&wo-QThLoz6KIjT7&H{{ELIOtXs@$#D3 z|F1IySZ7mri{w(m=mpipg-3=EEeiQiwaMOAChX{1QMht*gT9uS(~hr^Bo8j1QB26> zJv_y#_}wz$@qgeoL#y`x-eeNCYG?u_$A-d%tt5N`PWJ-4D=KGl^jbrJvO*!IC`#|FOQX;csuowLnvd=7M zEIRdIwwWo5!4_TRO-zT8r^)DvrwI97MBVtjS&$mo$#!$Eywj;^#ynZsDn(iOO*vqz ze46YT@9Tdorghl1l?$GcHDjs}llUSqrmm+-{eWYnf|cm=0?kt~hF{_Kk&?7w69r@P zhYy!c;QuCntLCH!rYbWwsl8Kn!-(b$Y_V2!2?(xJmcz0Tbp}aI!w5l#dTU#Xz2oil zP`4E9FrNV?obGOk_P0AM27uNp*H_0#)*MFBqtuGnt%)QO*M(CZmgL;c`XWXMIh6(8 zAddGzZ@MDc!;q|eYpsb-l1V}2L#SACn8zz zA?kf?T+(Dv@+xg-gTl7ea@Ggy%-y;m8suAEo{WQVF6Y)~GQ2guN5?_{C&J|bnk{vA z%n=5Jvw?*LGs)B+C1Z5uOS7Mx;s#&i?)>u5&S9mz?nyMkH|wof1A+gVx-#Dszmo6mYEvb)p<+|VH{1IC2F_p0w4Nt z9KX-ebyU6P`Ba5#KHfcMe=@+m#jNeEA2*bh?ksa4IK@fi>{wJv?Gqy)GrThKx*1Dh`d;i z(UV>P^A=jXvK}7rOn`*&>?$?r8AZ)g-`(B;`DKSyC(`vGt&Y&R3PJm9zWn#18PxZ+ zE(vV8ZY@&)tQhmL+JU8?vpdz?Pm@@(9(W7s6Z`9l-6Dgs^U9WYIVhAU+IgOCXrAw^ z;_d&b_W8sR!Yrl&8u59iW02rc>~V4OveZX^ zZF3L>vtGPQin+7@Cqm}KR}+hil*xu8x_?hnE89=PBo~2$)_U67cBGNx7m<;Qt$d=duVv z#oeNwqX>V8I8;cdp{sq-m+c8GCr~^EuTC4r%K%|c0Ht%t4f8wZZI+66;TOIV+S&(3 zjE_q0TD5#7(E-JNlIfE(PP1D*TTkV}stbh-_R2p_KXpi~j&AtISEk+xNposN-|I0? z97?MkRB_xkJh>j8Xsrpy? ztGbS+-@vRE9ie8(ZFN_n3WfA}jN*DR`&aU1Z_A0}gg7}-mAorSa;q5{z@mr~EuTfS^k4=FthzKBJ`suGQVZlB`6f@#x|E$Lbls?P(cOXAd)fN}(RVM}D zF%<@{%|WG`~ZEc?}#rWzC$S>-QX?dj)rdDi-17DRYI~A2N;f28Y1TyVB=B(#w z5gIeM#olQM$j*yail^FVzO|2!N&6_Tw0jOD+j5rys%k*?s(su;GMg|L1TRS^KUQ?>%PpXDi->I)Oz(dbLuK8Qh0C-wV4;jdJ_%E`4J}UaS3W z>Uw3*G;lz9OWw&Fk~!x7+c_Kmztik)Q5%$Oj$2&5hov!DKZDBeV0PO_VATOuEj&8P zq}ze$i;PM>E*+h%@fHN@S-#a+CT1dk8jH=bFgAYA3i=rz4wk2?GOTHtK%?KAI|h$r zyb93vhr4(OiY>60ASRTwnzE2iNx2Lvn`E8Ck2ni-B>mny8FNK(B}c^AhkoFG@(<ty-q0o8ne!Q67WYLP^T)esnNA(qJU@2?QQOp!`tdW z4AM9n*UUl|_VtD}=yf3%k$$B@<#*Xdp+2g{qmdb^!Lc4kiIVCceAXZeE9XIWr8A9) zg(x3cj@;J7j}H+oSCz9ApU$Hx2v0bw7+8W9fEGcQ(jX;>DjAbME^8nms2IH(jj-Mx z^diOIJk7{whU`HL^K%zbtEMlEmU0i{__$k%GZB&F%p(2$;wUI+WuA%9e-3j zaoAo#Qs6OEzHb*B`94-|Kl|UHurH~xsPj(3d*8BB^*t8-2^UEPp(0OMlLw&URBH%S zM|?wk6U$ZK56TKspicHQW_<2Ld2(JZlPu{4K)S*EXO#m_@PaZil>;&9#gYAMK#50A z4V>}ORZmBx+_0+HI;smVwGEgzWzKqiAZo)9<=Q1)$6PCH^U#(4k6%bw4A?&GiQD7n#g+5^y^@WRvQjP_Vl&P@W4h+&8qy1{VKF26Ts(VdJ`zPK&5^K zd9O6rHc$G^|sG_CrPGi)Hh0M36t0$d&=!+M6I|AWlZdq+UL88vQg z`j&>9m`)bY*!*!|(a}iASWURMR{JW_Z#!bUwn)%#I0XIMy{8HGkPP42P17s~@M=h~ z>ky@+s0s-tSln{IG~SQeGF&BSU^$*SYoCxeqR$b+$}d)`pGAG>Q(amr-mTq>QR!bc zrzRSOC;Yq-aFx$X12Y1~h|Zh=IONOIANEyMA1@T1Oz1zZzA=)3Hj=95JV1~TJe^Bk z&D_IAUKOX+;)t|%o9)>&rtN!52T8e;v@~zsg|40%#Wv)R^7iZp9V`FZx!FXwL%Vn7 zaRm_A9uKpnrrwK5!RGTmyj3hd1R9QVO2LZsuxy zh`F`O%chnP=c)Eo`@x8sGi+lWKyE<^2DzG|vl)2N-AAH|EQHYHx>p!ysm_rlaBUHUGyJ&${YHqU_0) zUv<{hL&+-~2TQg#LNrEW0Z+nEfj=JZ5#>kf4JPI_SZ7prw9?3@ov^NX-2tP3MjINVwxTAl9pbqnvD?X5TO8vmPM0-FQA<6liE3GQ8V2BGhL0Di(?2i5W5;sdu)*b}l zwqbx_WiNg(#wuJ5p~E#PQR;hRoP<$4b! z7lWry!BcipDzKf}t46qIL*TR5z_F|eOBTHzd@^C%80^SKvXjbMm!UDnJvEt^8!sjd zZOVcds_?Rdo(hLik332Mk#f;#sjR12bNqy<5`WWWB+vej#!2=(4z_wdG&~_{=xi3g@dh9k-FLQW6VZ81s?(3(QQ0BPb5Gi>+)zlP)DxMs#)K2ZNK-{P|zSN{am-h{RGfNIfP(Mm}41i zW7}o@dwo2Ag<@#Vj|Wgoi)aFV&O#q=mXW3Ga_FHj?J2xd=G`c2s&7myU5q`=7%0$5 zN%D9Cq+dK?!)AV>&@4JSscQhpFH(YsHLT_v`jMc&aU2Fz4}~G~Fv&c9!{KlVN-~gt^qtOb5U=b+hJ|h2qgBB_aoeCx z4j-fKT%U&~6sN@*Ndqlj8F`{QrgvG_pv5WtuPX$D8SZBl1G$05LFssapa?fq7krKkh2y|8mL8pQFvZcv6?hgX&SZ5 zUx2~bADtwYeKRSuu`6kaXHMJ(RjE`nX0Lmoiu;3?Ar6DLk8v??dq{243G0yyKOp%GBA_H~6W>(f5#ncuzR z>tNm(qPHx+UuOS)s7^L`iQJ)-Gm<xNn>B1B`Z-CI$F^?1 zyKHkwX`13E4BErYy^9{dnH-ZuE;(6OR|?pnehb9_3l1>IR=2|`p|xnj4$Y)n_?(5z zZX?IUAxjUgwGbw`va${R%hcN<1lNq6hI0B4}w{6F*$x& z{lQ6O%@scu|8y&6g`2!Y08!le4YS7q^>&lKe6Ctd(9G?;8Mi-O38L1EoQAeld+cN1 z#q~*syVmM{OM7KG&C?4M@e*~N#qm7Y{UF}4w}^T)u=X!G78&m+7jo={T6Hfq&-t;& z@+MS@@+WHR)L`=N{Euo6;T<9Vh+5%+){DS-w)z6KI>gQ$vFxSVgBiQCPnl_gTH$kq zK)}Et0Pcot-$lE!RSf388VcZL7r%LOU*G_ZjSg*++st2&adxkf;|Oq+uIbrv8xwb` zyL{QLrK`7&i*00D6SE=SLb8^=naQ`l365#srpmh4m?P}_Y(x*PYko0o{|%Q6jpG9f zU;$~iw3IP8MywL9mXw(fzBk2wY~>-r8+(2Mknl$2M5u1tk=Yj$>VqEqy_U=1n|uVo zA$qvJ!W0|Lapo@WHTj8$vAr}1cns9{W}N^#9OTYO4btE95l4Xv!ln4lg9o&neUW3}3_5hU9*izFsH%r!&o8#l z2C#Kwp*cy*LO|pRMYIA8Zur`%N4m_tk?Mic@AbkN-;ncKw z7v`7YU>n0FZtS?Ir)nyPr#QSv``3ZG-V*r$59GPuHsK|T=I`mD56ldWsY`VK z<*Sk3Z|=}dC(bo7uoD>zX!G&$1=?QF`m99FETK1TpE|KQ7OuC^>l}{S2dEOqk(?JZ z%wv#GASG^3shi6+u0Il^Ng(Q9V=u<%-?2S~|hMTmPe^YoRpZs@Jw@wut4=rUVTzMf6d4O%~% zUHjGoZ|q$aX1h?TNG(fGLv-k=1JwzTRxo|-j1vUt_lloy0-6Ty)IXtC$hwvt;%j7) zHt|cS@zuy%KMq!Hs2g39<~Nmv52fW400>4Ok04gyIA;HS+_(~^HlmuYTY;9n(A{Kb zza8u^n4Al8);8Ykex`Lc1Y5<42At3Q)vkLFN?P1?m4mF;n~?{*s?vBq zL7QHSststs4jLvf)- z_rAgzbSa){i1 z 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 literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hi;2MYvMt$SQP;xz=dFx){*RWnv{mVKkiQ?V zdVcyc-^*+j$Ib_rS82OFyzp9feZq3v{D;kfhVv2+>fJLr{ag7;#nI{AvrgVvIbFS2 zC$N>J^Ii4ZFM>w=?Cg?T*RVxunlQXQ@TF!#&B9JCAKAoPszr4oCEU?1O)uML`iiUw zPONeEJoW1!3-6u3k+bz@cB>^9Nhm%(>h{7{M)5M2W3iK3^{Hde7EF-HoPBuniapDs zdU9&*Q$5yZ`%ke=vu9-5xmqh*#cARtIgQceD8YNW`njxgN@xNA D=o26P diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png index ba8ed8c12cf19a82e8109f4d7b46e10b9afcff99..a98fe63b0930e9e9358059dd6661e1eaadfd73fe 100644 GIT binary patch delta 521 zcmV+k0`~or0+$4k8Gi-<001BJ|6u?C010qNS#tmY2^9bU2^9epf*r&F000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs0004`Nkl*YzRL{|`7)pTLb9a9uA|C+U{jP{?nc1_d zs;V?igR0&v%YX8nWLefTv#(Lx$7c3;JRYAdYaxWNWoAb}$KAVXW;+plz6u6tn&tq= z09Dl;1@1-p*Q?-}zD4K0a`9En?)qsBjH-T){F@3$l=pv;MY;QeU%40(07T^Wx&pwR z$p1ru_faxmMTQV^;Azy&K+S9)FyL%9!b_E9*>y8}3x8ahg-lgniMx}g>4PLmPK%=W z8A8aLrgx?$?0dB2u{fplO1xTM*()12p#&C!%B_r;BAbp-{@684td9^#&Z%%wh1F4GQTUQP1&+u7*cRb27$&an8u6*ezg~ zZwgT~*TP5^d^Jok+S>?jbs+4y;UMVdxM%*GW4hn&*ry9n1R+;vOED-50ssI207*qo IM6N<$f_evhlK=n! diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png index f723d980e594a0042a9a94aac6aef9127ec3bf8c..253450ecbc102a345187896f2b65ab1900d8fcac 100644 GIT binary patch delta 1184 zcmV;R1Yi5A2B`^<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!3pV0000rPkH?m}pC4S`E^%>RVX83C>l269ubw}>Eh{EOljH8WiLK=cY6?qh?G5?4 zdUO1`oXWiCi7_xRF?qT;hIkzBopyWLDg&N2XGg)0I$B*y71t!^9==!kKU~_H$8+(8 zr{$rW{+Z;?TEo!qlKIB(e3j^zz3(2fl+F6gc2DxfS02;bfpPlzyc-T&j@J|OY4h7! zV{=?+sj5fGJ?-uM6L(r$oLKR+c*0aCW(}Dcc`TFkO#*LS5nJ{);$44J-?mnb8Fe#m zx-?p4J(l)Ly7pZyN}xgF7Twf^~iz^mV3Ir zv{xNHaqYWv+m&6PTyG|SlRhwgdsSMmi$c(?Rw+G|n|Jq%l`P=NdSU8ymhHg882ts) zSiJo{%)h4Vy?DZl6RZ!KS+!dIXMFC|DX}#CTU5!wJwN7n$#W4Mo{78W8f|0aR@QPj zFkPna$pJw(ec4Q=xSci3+^2&?i>5tGP<&R^8((EHKR&x(^?FsU!=mQEyZ>{-A2jU_ zoOLI9uH7e(#}cVc-YXK@)6#MzCV_|S*E^l&Yo9;Xs0003jNkl#UYIJ7F_P{lv99e)&;`Z@GmJYH&Ve-KDQ z@=J1Zf=Wi_)m`;Q{ZPNuJ9SU3W#eOGJOuUpAK&WQAcCTLG9)p@qdFNA6zF3csPl_1 za0HwOdl@JLTfi$Y1=N7!bmqA2Yqd1sZBi+#&mm`)Fn5agoMhCqD87*ZD^cr*z$3$P zAL;c3C`FEooPT71%SbOJz*yvP1cE>D3DAigiz5&$Mfy_$oJNjHPI3j|bip%rV7MRU2wc{ZzX_-%;nX@js(a3259AKP&(M00{s|MNUMnLSTaLAEsOY delta 253 zcmV+_A(1OFPEseh85xH83Ft7_ zLY;umhDDCiUIDf`0C;A2l<4NTr~a8^x?gVFS2&vj^r{wZl8^AF00000NkvXXu0mjf D1{!dZ diff --git a/res/tray-icon.ico b/res/tray-icon.ico index fd2e61628a2ae248de18c5492c90badad1025aa2..df8bdaccbd9df0c34ca00bb736ba2361f94e1705 100644 GIT binary patch literal 4286 zcmchb`)^c56vwA6G0_-bAi-*46MljCqpxUu(4e3ajs6uz{lUZ-P;4yKVnMVKCB)D+ z_z08`H3$_bExX$^fFRN;E%>CSw7c7FugmV;*L{9HbGLUpclXY27t_w>v@>(gna`a$ zvvcMyV>R??Xkh#=J8&Cg%Nb*sW_A>H6DdC7M$2Edi)sU9FvNfxT`u}_k0 zAaS|;e;dhSmsSFD_6v&*7I=Iefo=k_t+`ed)C3)$Y8$3U~DF7V$hozv=)Zk`wZEw44F2P?KQ|9SOY71 z&P(!dCH%XE-pB~-jdk;6Go*Jgq}mzOowdk@UIR=`80 zcx#>a!{~n##`(`kc0xP(Ix=6ZhAPb4Wyd=h{_Uc5IP;2UJZa;fGZgP7uv+VZ*k!L- zMxp)vr?*dN8RI9A+P4bDY-KNl@>MO2=^q7a|Ek@wer~U|t$4 za)=>v>{h9xzV+sk^W6OPfAIg>4Uru_%n&|ylbqjvcTp~(95eWi)V`8Jy|~F)b3Sj* zp6sC*gjP!3^!@9FJeEIGGy5Nw+E-F2!G|ESCyp{qpMP5FW(>SLn?t?lxfwkx^^qye z^jHb|YA@xav28LJ*wdqwzrAdp$I=HMp!|`Q`uQm=O+obdoz9x`d2{yFK8k1RlAn2@ zFWdciK7SYGl63D&u&aCqJu!rE-_im*HvCM1Bdb{9)0D?*3i-Q`OZG4%zNfqwljjgC zGXdN1UTO5?5GF&*FdnFbmXQ03&yx+j?A#qhHqW8%d4BEagGdd(jqKlD$Xw}!I?@Cs z^oSGhsT1`u)KPIbSb7Xbx{7OQURusc{$q^z79MVDUyXMeC!|zf9G|$A;#*q O@Sg}4xo~EP*#80bVj{T! literal 4286 zcmc&&T}V_x6rQcZM%El76HzThvO6B<#UW z`%x3pD2fc%wZup(BTQ06=^-+~ptZtE{hG7G=$$+FuCvOzv&@}4=g#+i=bV|HyR(dW z_$MKO(Uq0WVQc|oYz42}#1eTWd${mtn>?Zl!^jn0mc(m-*FIhmqe|R0NMYMXlo{hT zUyStJ<49o_$Oojd?Gvso^UL$VwQJ+1PjeB?d}k~K%J&0xdBCOBBDbgJiGGzA@GW=G zw~SeBe;CD{wG?=J4;UK6aCviEXjL2U)>zL4@EoFd|TN+zXqe=xGDeXPI*}m8f{?U0!2`FG$p}<@?OAGM3B)z8-kqt>x(%GY25XS zp}pN&zPvZX+4UO=tt>!iy&2E_11hdR1f)$j!&-fyd|Qj$udQR8?&;lOFSHjrjdin~ zo(~NEQ(2y!Gw1mBNyXCKrA;%(*X65t>gBID$3Hl%;~Q_8?SI8P^>@M77ISUNDaG2` zrTftO1h}}&nd`(%pgL2}JFygQb;h{OAK9;MaOR@@i1yd>iv?%;Y=GWzzrXALl7;dX zNhi{?(!F{vrAkw=;Jv#?TJ*N98aO^rVD0uDPS2>dQ+wp44+|ZnV zs8ru6&1FKr;-FL%ZEq(~lsvwerLMirUMbdCR|9Zkv(TX5rX4{(&wg&(!hdf~idH+8 z=dICv3GuZV9P|3L=O_ApfS(1E Date: Tue, 7 Feb 2023 21:14:01 +0800 Subject: [PATCH 395/734] add design.svg --- res/design.svg | 374 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 res/design.svg diff --git a/res/design.svg b/res/design.svg new file mode 100644 index 000000000..62e568242 --- /dev/null +++ b/res/design.svg @@ -0,0 +1,374 @@ + +rustdesk From 7820c890c569214fffbebc9382fd7adf35389cf1 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Tue, 7 Feb 2023 13:32:42 -0500 Subject: [PATCH 396/734] Small fix to README wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc9bacf19..62950846f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ ## Free Public Servers -Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow. +Below are the servers you are using for free, it may change over time. If you are not close to one of these, your network may be slow. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | From 9527d3b41d9408ea377084b1c5e3a018f0289f65 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Tue, 7 Feb 2023 13:33:38 -0500 Subject: [PATCH 397/734] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62950846f..866063726 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ ## Free Public Servers -Below are the servers you are using for free, it may change over time. If you are not close to one of these, your network may be slow. +Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | From cf121bdf47550df9725b604e340e1fa4491cafd4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 22:27:11 +0800 Subject: [PATCH 398/734] win, translate mode, not debug yet Signed-off-by: fufesou --- Cargo.lock | 26 ++++++++- Cargo.toml | 2 +- src/keyboard.rs | 105 ++++++++++++++++++++++++++++-------- src/server/input_service.rs | 17 +++++- src/ui_session_interface.rs | 21 ++++++++ 5 files changed, 144 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e15641363..4ac2720be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,6 +4401,28 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdev" +version = "0.5.0-2" +dependencies = [ + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", + "enum-map", + "epoll", + "inotify", + "lazy_static", + "libc", + "log", + "mio 0.8.5", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", + "winapi 0.3.9", + "x11 2.20.1", +] + [[package]] name = "rdev" version = "0.5.0-2" @@ -4709,7 +4731,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..5d75b7a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/fufesou/rdev" } +rdev = { path = "../rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/src/keyboard.rs b/src/keyboard.rs index 054a39580..bcb0650ac 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -92,7 +92,8 @@ pub mod client { if is_long_press(&event) { return; } - if let Some(key_event) = event_to_key_event(&event, lock_modes) { + + for key_event in event_to_key_events(&event, lock_modes) { send_key_event(&key_event); } } @@ -341,7 +342,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option { +pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -357,28 +358,38 @@ pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option map_keyboard_mode(event, key_event)?, - KeyboardMode::Translate => translate_keyboard_mode(event, key_event)?, + let mut key_events = match keyboard_mode { + KeyboardMode::Map => match map_keyboard_mode(event, key_event) { + Some(event) => [event].to_vec(), + None => Vec::new(), + }, + KeyboardMode::Translate => translate_keyboard_mode(event, key_event), _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { - legacy_keyboard_mode(event, key_event)? + legacy_keyboard_mode(event, key_event) } #[cfg(any(target_os = "android", target_os = "ios"))] { - None? + Vec::new() } } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(lock_modes) = lock_modes { - add_numlock_capslock_with_lock_modes(&mut key_event, lock_modes); - } else { - add_numlock_capslock_status(&mut key_event); + + if keyboard_mode != KeyboardMode::Translate { + for key_event in &mut key_events { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(lock_modes) = lock_modes { + add_numlock_capslock_with_lock_modes(key_event, lock_modes); + } else { + add_numlock_capslock_status(key_event); + } + } } - return Some(key_event); + println!("REMOVE ME ========================= key_events {:?}", &key_events); + + key_events } pub fn event_type_to_event(event_type: EventType) -> Event { @@ -386,6 +397,7 @@ pub fn event_type_to_event(event_type: EventType) -> Event { event_type, time: SystemTime::now(), name: None, + unicode: Vec::new(), code: 0, scan_code: 0, } @@ -423,13 +435,14 @@ pub fn get_peer_platform() -> String { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { +pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { + let mut events = Vec::new(); // legacy mode(0): Generate characters locally, look for keycode on other side. let (mut key, down_or_up) = match event.event_type { EventType::KeyPress(key) => (key, true), EventType::KeyRelease(key) => (key, false), _ => { - return None; + return events; } }; @@ -475,7 +488,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { if is_win && ctrl && alt { client::ctrl_alt_del(); - return None; + return events; } Some(ControlKey::Delete) } @@ -545,7 +558,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Some(ControlKey::Subtract), Key::KpPlus => Some(ControlKey::Add), Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return None; + return events; } Key::Home => Some(ControlKey::Home), Key::End => Some(ControlKey::End), @@ -628,12 +641,12 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option Option { @@ -703,6 +717,51 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { - None +#[cfg(target_os = "windows")] +fn is_modifier_code(scan_code: u32) -> bool { + match scan_code { + // Alt | AltGr | ControlLeft | ControlRight | ShiftLeft | ShiftRight | MetaLeft | MetaRight + 0x38 | 0xE038 | 0x1D | 0xE01D | 0x2A | 0x36 | 0xE05B | 0xE05C => true, + _ => false, + } +} + +#[cfg(target_os = "linux")] +fn is_modifier_code(key_code: u32) -> bool { + match scan_code { + 64 | 108 | 37 | 105 | 50 | 62 | 133 | 134 => true, + _ => false, + } +} + +#[cfg(target_os = "macos")] +fn is_modifier_code(key_code: u32) -> bool { + match scan_code { + 0x3A | 0x3D | 0x3B | 0x3E | 0x38 | 0x3C | 0x37 | 0x36 => true, + _ => false, + } +} + +pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { + #[cfg(target_os = "windows")] + let is_modifier = is_modifier_code(event.scan_code); + #[cfg(target_os = "linux")] + let is_modifier = is_modifier_code(event.key_code); + #[cfg(target_os = "macos")] + let is_modifier = is_modifier_code(event.key_code); + + let mut events: Vec = Vec::new(); + if is_modifier { + if let Some(evt) = map_keyboard_mode(event, key_event) { + events.push(evt); + } + return events; + } + + for unicode in &event.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*unicode as _); + events.push(evt); + } + events } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 2715a2643..072ef53fb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1067,6 +1067,21 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { release_keys(&mut en, &to_release); } +fn translate_keyboard_mode(evt: &KeyEvent) { + match evt.union { + Some(key_event::Union::Unicode(unicode)) => { + println!("REMOVE ME ========================= simulate_unicode {}", unicode); + allow_err!(rdev::simulate_unicode(unicode as _)); + }, + Some(key_event::Union::Chr(..)) => { + map_keyboard_mode(evt) + } + _ => { + log::debug!("Unreachable. Unexpected key event {:?}", &evt); + } + } +} + pub fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { return; @@ -1080,7 +1095,7 @@ pub fn handle_key_(evt: &KeyEvent) { map_keyboard_mode(evt); } KeyboardMode::Translate => { - legacy_keyboard_mode(evt); + translate_keyboard_mode(evt); } _ => { legacy_keyboard_mode(evt); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4fc5db743..95b8cdbd0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -361,11 +361,31 @@ impl Session { } pub fn enter(&self) { + #[cfg(target_os = "windows")] + { + match &self.lc.read().unwrap().keyboard_mode as _ { + "legacy" => { + println!("REMOVE ME =========================== enter legacy "); + rdev::set_get_key_name(true); + } + "translate" => { + println!("REMOVE ME =========================== enter translate "); + rdev::set_get_key_name(true); + } + _ => {} + } + } + IS_IN.store(true, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Run); } pub fn leave(&self) { + #[cfg(target_os = "windows")] + { + println!("REMOVE ME =========================== leave "); + rdev::set_get_key_name(false); + } IS_IN.store(false, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Wait); } @@ -429,6 +449,7 @@ impl Session { let event = Event { time: std::time::SystemTime::now(), name: Option::Some(name.to_owned()), + unicode: Vec::new(), code: keycode as _, scan_code: scancode as _, event_type: event_type, From 6e54cd2e6b7a7e207b13e31f2668db4df98f13ee Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 10:41:47 +0800 Subject: [PATCH 399/734] win, translate mode, check dead code Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 29 +++++---- src/common.rs | 4 +- src/keyboard.rs | 63 ++++++------------- src/ui_session_interface.rs | 5 +- 4 files changed, 41 insertions(+), 60 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 36b9504c0..4fd702ad8 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1382,25 +1382,23 @@ class _RemoteMenubarState extends State { text: translate('Ratio'), optionsGetter: () { List list = []; - List modes = ["legacy"]; + List modes = [ + KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), + KeyboardModeMenu(key: 'map', menu: 'Map mode'), + KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), + ]; - if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) { - modes.add("map"); - } - - for (String mode in modes) { - if (mode == "legacy") { + for (KeyboardModeMenu mode in modes) { + if (bind.sessionIsKeyboardModeSupported( + id: widget.id, mode: mode.key)) { list.add(MenuEntryRadioOption( - text: translate('Legacy mode'), value: 'legacy')); - } else if (mode == "map") { - list.add(MenuEntryRadioOption( - text: translate('Map mode'), value: 'map')); + text: translate(mode.menu), value: mode.key)); } } return list; }, curOptionGetter: () async { - return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy'; }, optionSetter: (String oldValue, String newValue) async { await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); @@ -1689,3 +1687,10 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); } } + +class KeyboardModeMenu { + final String key; + final String menu; + + KeyboardModeMenu({required this.key, required this.menu}); +} diff --git a/src/common.rs b/src/common.rs index c2d5a81f0..8f8ce8dec 100644 --- a/src/common.rs +++ b/src/common.rs @@ -671,8 +671,8 @@ pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: match keyboard_mode { KeyboardMode::Legacy => true, KeyboardMode::Map => version_number >= hbb_common::get_version_number("1.2.0"), - KeyboardMode::Translate => false, - KeyboardMode::Auto => false, + KeyboardMode::Translate => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Auto => version_number >= hbb_common::get_version_number("1.2.0"), } } diff --git a/src/keyboard.rs b/src/keyboard.rs index bcb0650ac..7d5f36af2 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -387,7 +387,10 @@ pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec Event { Event { event_type, time: SystemTime::now(), - name: None, - unicode: Vec::new(), + unicode: None, code: 0, scan_code: 0, } @@ -571,7 +573,8 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { if s.len() <= 2 { // exclude chinese characters @@ -717,51 +720,25 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option bool { - match scan_code { - // Alt | AltGr | ControlLeft | ControlRight | ShiftLeft | ShiftRight | MetaLeft | MetaRight - 0x38 | 0xE038 | 0x1D | 0xE01D | 0x2A | 0x36 | 0xE05B | 0xE05C => true, - _ => false, - } -} - -#[cfg(target_os = "linux")] -fn is_modifier_code(key_code: u32) -> bool { - match scan_code { - 64 | 108 | 37 | 105 | 50 | 62 | 133 | 134 => true, - _ => false, - } -} - -#[cfg(target_os = "macos")] -fn is_modifier_code(key_code: u32) -> bool { - match scan_code { - 0x3A | 0x3D | 0x3B | 0x3E | 0x38 | 0x3C | 0x37 | 0x36 => true, - _ => false, - } -} - pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - #[cfg(target_os = "windows")] - let is_modifier = is_modifier_code(event.scan_code); - #[cfg(target_os = "linux")] - let is_modifier = is_modifier_code(event.key_code); - #[cfg(target_os = "macos")] - let is_modifier = is_modifier_code(event.key_code); - let mut events: Vec = Vec::new(); - if is_modifier { + match &event.unicode { + Some(unicode_info) => { + if !unicode_info.is_dead { + for code in &unicode_info.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*code as _); + events.push(evt); + } + } + } + None => {} + } + if events.is_empty() { if let Some(evt) = map_keyboard_mode(event, key_event) { events.push(evt); } return events; } - - for unicode in &event.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*unicode as _); - events.push(evt); - } events } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 95b8cdbd0..3801eda67 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -423,7 +423,7 @@ impl Session { pub fn handle_flutter_key_event( &self, - name: &str, + _name: &str, keycode: i32, scancode: i32, lock_modes: i32, @@ -448,8 +448,7 @@ impl Session { }; let event = Event { time: std::time::SystemTime::now(), - name: Option::Some(name.to_owned()), - unicode: Vec::new(), + unicode: None, code: keycode as _, scan_code: scancode as _, event_type: event_type, From 6eec0041bd7038a062a250b7f6dbd8bb3b4569d1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 6 Feb 2023 16:26:27 +0800 Subject: [PATCH 400/734] win, tranlsate mode, handle shift Signed-off-by: fufesou --- src/keyboard.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 7d5f36af2..08ab23b1b 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -88,14 +88,17 @@ pub mod client { } } - pub fn process_event(event: &Event, lock_modes: Option) { + pub fn process_event(event: &Event, lock_modes: Option) -> KeyboardMode { + let keyboard_mode = get_keyboard_mode_enum(); + if is_long_press(&event) { - return; + return keyboard_mode; } - for key_event in event_to_key_events(&event, lock_modes) { + for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) { send_key_event(&key_event); } + keyboard_mode } pub fn get_modifiers_state( @@ -205,7 +208,14 @@ pub fn start_grab_loop() { return Some(event); } if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(&event, None); + let keyboard_mode = client::process_event(&event, None); + if keyboard_mode == KeyboardMode::Translate { + // shift + if event.scan_code == 0x2A { + return Some(event); + } + } + if is_press { return None; } else { @@ -342,7 +352,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec { +pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_modes: Option) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -356,7 +366,6 @@ pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec {} } - let keyboard_mode = get_keyboard_mode_enum(); key_event.mode = keyboard_mode.into(); let mut key_events = match keyboard_mode { KeyboardMode::Map => match map_keyboard_mode(event, key_event) { From ddc9792d15420a2a3e56cc6b37cc89a064c5013b Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 6 Feb 2023 18:13:17 +0800 Subject: [PATCH 401/734] win, translate mode, debug almost done Signed-off-by: fufesou --- Cargo.lock | 28 ++----------------- Cargo.toml | 2 +- .../lib/desktop/widgets/remote_menubar.dart | 6 ++++ src/keyboard.rs | 9 ++---- src/server/input_service.rs | 1 - src/ui_session_interface.rs | 11 ++------ 6 files changed, 14 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ac2720be..988363019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", + "rdev", "serde 1.0.149", "serde_derive", "tfc", @@ -4404,29 +4404,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -dependencies = [ - "cocoa", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.3", - "core-graphics 0.22.3", - "enum-map", - "epoll", - "inotify", - "lazy_static", - "libc", - "log", - "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.1", -] - -[[package]] -name = "rdev" -version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#238c9778da40056e2efda1e4264355bc89fb6358" +source = "git+https://github.com/fufesou/rdev#77b45e9e43f713851874c7fbb8e7149ab4f2e6a1" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4731,7 +4709,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev 0.5.0-2", + "rdev", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 5d75b7a23..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { path = "../rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4fd702ad8..c3c8ce3fe 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1391,6 +1391,12 @@ class _RemoteMenubarState extends State { for (KeyboardModeMenu mode in modes) { if (bind.sessionIsKeyboardModeSupported( id: widget.id, mode: mode.key)) { + if (mode.key == 'translate') { + if (!Platform.isWindows || + widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) { + continue; + } + } list.add(MenuEntryRadioOption( text: translate(mode.menu), value: mode.key)); } diff --git a/src/keyboard.rs b/src/keyboard.rs index 08ab23b1b..fd9514427 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -210,8 +210,8 @@ pub fn start_grab_loop() { if KEYBOARD_HOOKED.load(Ordering::SeqCst) { let keyboard_mode = client::process_event(&event, None); if keyboard_mode == KeyboardMode::Translate { - // shift - if event.scan_code == 0x2A { + // SHIFT(0x2A) RSHIFT(0x36) + if event.scan_code == 0x2A || event.scan_code == 0x36 { return Some(event); } } @@ -396,11 +396,6 @@ pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_mode } } - println!( - "REMOVE ME ========================= key_events {:?}", - &key_events - ); - key_events } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 072ef53fb..1d7d4773d 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1070,7 +1070,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { - println!("REMOVE ME ========================= simulate_unicode {}", unicode); allow_err!(rdev::simulate_unicode(unicode as _)); }, Some(key_event::Union::Chr(..)) => { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 3801eda67..12412d7cd 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -364,14 +364,8 @@ impl Session { #[cfg(target_os = "windows")] { match &self.lc.read().unwrap().keyboard_mode as _ { - "legacy" => { - println!("REMOVE ME =========================== enter legacy "); - rdev::set_get_key_name(true); - } - "translate" => { - println!("REMOVE ME =========================== enter translate "); - rdev::set_get_key_name(true); - } + "legacy" => rdev::set_get_key_name(true), + "translate" => rdev::set_get_key_name(true), _ => {} } } @@ -383,7 +377,6 @@ impl Session { pub fn leave(&self) { #[cfg(target_os = "windows")] { - println!("REMOVE ME =========================== leave "); rdev::set_get_key_name(false); } IS_IN.store(false, Ordering::SeqCst); From 347add18744358b9d07247bee458f2a4ca17c0f8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 09:48:04 +0800 Subject: [PATCH 402/734] win, translate mode, to debug Signed-off-by: fufesou --- Cargo.lock | 26 +++++++++- Cargo.toml | 2 +- src/keyboard.rs | 100 ++++++++++++++++++++++++++++++++---- src/server/input_service.rs | 13 ++++- 4 files changed, 128 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 988363019..f5ffa7f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,6 +4401,28 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdev" +version = "0.5.0-2" +dependencies = [ + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", + "enum-map", + "epoll", + "inotify", + "lazy_static", + "libc", + "log", + "mio 0.8.5", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", + "winapi 0.3.9", + "x11 2.20.1", +] + [[package]] name = "rdev" version = "0.5.0-2" @@ -4709,7 +4731,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..5d75b7a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/fufesou/rdev" } +rdev = { path = "../rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/src/keyboard.rs b/src/keyboard.rs index fd9514427..492314ab6 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -199,6 +199,9 @@ pub fn update_grab_get_key_name() { }; } +#[cfg(target_os = "windows")] +static mut IS_LAST_0X021D: bool = false; + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { @@ -210,12 +213,22 @@ pub fn start_grab_loop() { if KEYBOARD_HOOKED.load(Ordering::SeqCst) { let keyboard_mode = client::process_event(&event, None); if keyboard_mode == KeyboardMode::Translate { - // SHIFT(0x2A) RSHIFT(0x36) - if event.scan_code == 0x2A || event.scan_code == 0x36 { - return Some(event); + #[cfg(target_os = "windows")] + match event.scan_code { + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} + } + #[cfg(target_os = "windows")] + unsafe { + IS_LAST_0X021D = event.scan_code == 0x021D; } } - + if is_press { return None; } else { @@ -352,7 +365,11 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_modes: Option) -> Vec { +pub fn event_to_key_events( + event: &Event, + keyboard_mode: KeyboardMode, + lock_modes: Option, +) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -577,7 +594,10 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { if s.len() <= 2 { @@ -724,8 +744,7 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Vec { - let mut events: Vec = Vec::new(); +fn try_fill_unicode(event: &Event, key_event: &KeyEvent, events: &mut Vec) { match &event.unicode { Some(unicode_info) => { if !unicode_info.is_dead { @@ -738,8 +757,71 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec {} } +} + +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down() -> bool { + if rdev::get_modifier(Key::ControlLeft) || rdev::get_modifier(Key::ControlRight) { + return true; + } + if rdev::get_modifier(Key::Alt) || rdev::get_modifier(Key::AltGr) { + return true; + } + if rdev::get_modifier(Key::MetaLeft) || rdev::get_modifier(Key::MetaRight) { + return true; + } + 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 _); + Some(key_event) +} + +pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { + let mut events: Vec = Vec::new(); + #[cfg(target_os = "windows")] + unsafe { + if IS_LAST_0X021D { + if event.scan_code == 0xE038 { + return events; + } + } + } + + #[cfg(target_os = "windows")] + if !is_hot_key_modifiers_down() { + try_fill_unicode(event, &key_event, &mut events); + } + if events.is_empty() { - if let Some(evt) = map_keyboard_mode(event, key_event) { + if let Some(evt) = translate_virtual_keycode(event, key_event) { events.push(evt); } return events; diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 1d7d4773d..133b9a830 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1067,13 +1067,24 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { release_keys(&mut en, &to_release); } +#[cfg(target_os = "windows")] +fn translate_process_virtual_keycode(vk: u32, down: bool) { + let scancode = rdev::vk_to_scancode(vk); + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(scancode as u64 + KEY_CHAR_START, down); + + crate::platform::windows::try_change_desktop(); + sim_rdev_rawkey(scancode, down); +} + fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { allow_err!(rdev::simulate_unicode(unicode as _)); }, Some(key_event::Union::Chr(..)) => { - map_keyboard_mode(evt) + #[cfg(target_os = "windows")] + translate_process_virtual_keycode(evt.chr(), evt.down) } _ => { log::debug!("Unreachable. Unexpected key event {:?}", &evt); From 1294103ba778016a118ba51cbf9a3d61f1db5212 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 13:31:49 +0800 Subject: [PATCH 403/734] win, translate mode, debug Signed-off-by: fufesou --- src/keyboard.rs | 62 +++++++++++++++++++------------- src/server/input_service.rs | 71 ++++++++++++++++++++++--------------- 2 files changed, 80 insertions(+), 53 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 492314ab6..bdf1c5c1b 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -200,7 +200,7 @@ pub fn update_grab_get_key_name() { } #[cfg(target_os = "windows")] -static mut IS_LAST_0X021D: bool = false; +static mut IS_0X021D_DOWN: bool = false; pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] @@ -210,33 +210,43 @@ pub fn start_grab_loop() { if key == Key::CapsLock || key == Key::NumLock { return Some(event); } - if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - let keyboard_mode = client::process_event(&event, None); - if keyboard_mode == KeyboardMode::Translate { - #[cfg(target_os = "windows")] - match event.scan_code { - 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), - 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), - 0x38 => rdev::set_modifier(Key::Alt, is_press), - 0xE038 => rdev::set_modifier(Key::AltGr, is_press), - 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), - 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), - _ => {} - } - #[cfg(target_os = "windows")] - unsafe { - IS_LAST_0X021D = event.scan_code == 0x021D; - } - } + let mut _keyboard_mode = KeyboardMode::Map; + let scan_code = event.scan_code; + let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + _keyboard_mode = client::process_event(&event, None); if is_press { - return None; + None } else { - return Some(event); + Some(event) } } else { - return Some(event); + Some(event) + }; + + #[cfg(target_os = "windows")] + match scan_code { + 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), + 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + // Right Alt + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} } + + #[cfg(target_os = "windows")] + unsafe { + // AltGr + if scan_code == 0x021D { + IS_0X021D_DOWN = is_press; + } + } + + return res; }; let func = move |event: Event| match event.event_type { EventType::KeyPress(key) => try_handle_keyboard(event, key, true), @@ -808,7 +818,11 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec = Vec::new(); #[cfg(target_os = "windows")] unsafe { - if IS_LAST_0X021D { + if event.scan_code == 0x021D { + return events; + } + + if IS_0X021D_DOWN { if event.scan_code == 0xE038 { return events; } @@ -816,7 +830,7 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec ResultType<()> Ok(()) } +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +enum KeysDown { + RdevKey(RawKey), + EnigoKey(u64) +} + lazy_static::lazy_static! { static ref ENIGO: Arc> = { Arc::new(Mutex::new(Enigo::new())) }; - static ref KEYS_DOWN: Arc>> = Default::default(); + static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_PEER_INPUT_CURSOR: Arc> = Default::default(); static ref LATEST_SYS_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); } @@ -375,12 +380,7 @@ fn record_key_is_control_key(record_key: u64) -> bool { #[inline] fn record_key_is_chr(record_key: u64) -> bool { - KEY_RDEV_START <= record_key && record_key < KEY_CHAR_START -} - -#[inline] -fn record_key_is_rdev_layout(record_key: u64) -> bool { - KEY_CHAR_START <= record_key + record_key < KEY_CHAR_START } #[inline] @@ -396,15 +396,18 @@ fn record_key_to_key(record_key: u64) -> Option { } #[inline] -fn release_record_key(record_key: u64) { +fn release_record_key(record_key: KeysDown) { let func = move || { - if record_key_is_rdev_layout(record_key) { - simulate_(&EventType::KeyRelease(RdevKey::Unknown( - (record_key - KEY_RDEV_START) as _, - ))); - } else if let Some(key) = record_key_to_key(record_key) { - ENIGO.lock().unwrap().key_up(key); - log::debug!("Fixed {:?} timeout", key); + match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + } } }; @@ -733,7 +736,7 @@ pub fn reset_input_ondisconn() { } } -fn sim_rdev_rawkey(code: u32, keydown: bool) { +fn sim_rdev_rawkey_position(code: u32, keydown: bool) { #[cfg(target_os = "windows")] let rawkey = RawKey::ScanCode(code); #[cfg(target_os = "linux")] @@ -744,6 +747,23 @@ fn sim_rdev_rawkey(code: u32, keydown: bool) { #[cfg(target_os = "macos")] let rawkey = RawKey::MacVirtualKeycode(code); + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { + #[cfg(target_os = "windows")] + let rawkey = RawKey::WinVirtualKeycode(code); + + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + let event_type = if keydown { EventType::KeyPress(RdevKey::RawKey(rawkey)) } else { @@ -874,9 +894,6 @@ fn sync_numlock_capslock_status(key_event: &KeyEvent) { } fn map_keyboard_mode(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. - record_pressed_key(evt.chr() as u64 + KEY_CHAR_START, evt.down); - #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -894,7 +911,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } - sim_rdev_rawkey(evt.chr(), evt.down); + sim_rdev_rawkey_position(evt.chr(), evt.down); } #[cfg(target_os = "macos")] @@ -1011,7 +1028,7 @@ fn release_keys(en: &mut Enigo, to_release: &Vec) { } } -fn record_pressed_key(record_key: u64, down: bool) { +fn record_pressed_key(record_key: KeysDown, down: bool) { let mut key_down = KEYS_DOWN.lock().unwrap(); if down { key_down.insert(record_key, Instant::now()); @@ -1050,12 +1067,12 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { return; } let record_key = ck.value() as u64; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_control_key(&mut en, &ck, down) } Some(key_event::Union::Chr(chr)) => { let record_key = chr as u64 + KEY_CHAR_START; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_chr(&mut en, chr, down) } Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), @@ -1069,12 +1086,8 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] fn translate_process_virtual_keycode(vk: u32, down: bool) { - let scancode = rdev::vk_to_scancode(vk); - // map mode(1): Send keycode according to the peer platform. - record_pressed_key(scancode as u64 + KEY_CHAR_START, down); - crate::platform::windows::try_change_desktop(); - sim_rdev_rawkey(scancode, down); + sim_rdev_rawkey_virtual(vk, down); } fn translate_keyboard_mode(evt: &KeyEvent) { From 5c7f2678fa870427975bdc98c29c7126bb35ba58 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:23:05 +0800 Subject: [PATCH 404/734] update rdev Signed-off-by: fufesou --- Cargo.lock | 26 ++------------------------ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5ffa7f9d..988363019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", + "rdev", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,28 +4401,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "rdev" -version = "0.5.0-2" -dependencies = [ - "cocoa", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.3", - "core-graphics 0.22.3", - "enum-map", - "epoll", - "inotify", - "lazy_static", - "libc", - "log", - "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.1", -] - [[package]] name = "rdev" version = "0.5.0-2" @@ -4731,7 +4709,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev 0.5.0-2", + "rdev", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 5d75b7a23..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { path = "../rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } From 01f762ffdb57d7dee4a17110d293252b1621c49a Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:14:13 +0800 Subject: [PATCH 405/734] build linux Signed-off-by: fufesou --- src/keyboard.rs | 5 ++++- src/server/input_service.rs | 33 ++++++++++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index bdf1c5c1b..91480ba30 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -830,10 +830,13 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec ResultType<()> #[derive(Copy, Clone, PartialEq, Eq, Hash)] enum KeysDown { RdevKey(RawKey), - EnigoKey(u64) + EnigoKey(u64), } lazy_static::lazy_static! { @@ -397,16 +397,14 @@ fn record_key_to_key(record_key: u64) -> Option { #[inline] fn release_record_key(record_key: KeysDown) { - let func = move || { - match record_key { - KeysDown::RdevKey(raw_key) => { - simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); - } - KeysDown::EnigoKey(key) => { - if let Some(key) = record_key_to_key(key) { - ENIGO.lock().unwrap().key_up(key); - log::debug!("Fixed {:?} timeout", key); - } + let func = move || match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); } } }; @@ -758,12 +756,10 @@ fn sim_rdev_rawkey_position(code: u32, keydown: bool) { simulate_(&event_type); } +#[cfg(target_os = "windows")] fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { - #[cfg(target_os = "windows")] let rawkey = RawKey::WinVirtualKeycode(code); - record_pressed_key(KeysDown::RdevKey(rawkey), keydown); - let event_type = if keydown { EventType::KeyPress(RdevKey::RawKey(rawkey)) } else { @@ -941,10 +937,11 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { #[cfg(target_os = "linux")] fn is_altgr_pressed() -> bool { + let altgr_rawkey = RawKey::LinuxXorgKeycode(ControlKey::RAlt.value() as _); KEYS_DOWN .lock() .unwrap() - .get(&(ControlKey::RAlt.value() as _)) + .get(&KeysDown::RdevKey(altgr_rawkey)) .is_some() } @@ -1093,9 +1090,11 @@ fn translate_process_virtual_keycode(vk: u32, down: bool) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { + #[cfg(target_os = "windows")] allow_err!(rdev::simulate_unicode(unicode as _)); - }, - Some(key_event::Union::Chr(..)) => { + } + Some(key_event::Union::Chr(..)) => + { #[cfg(target_os = "windows")] translate_process_virtual_keycode(evt.chr(), evt.down) } From 586f0a272663222c6d013aca3129c4be0dfd0fae Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:17:37 +0800 Subject: [PATCH 406/734] compile macos Signed-off-by: fufesou --- src/server/input_service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 7b6130ad1..edf0ef497 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1089,9 +1089,9 @@ fn translate_process_virtual_keycode(vk: u32, down: bool) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { - Some(key_event::Union::Unicode(unicode)) => { + Some(key_event::Union::Unicode(_unicode)) => { #[cfg(target_os = "windows")] - allow_err!(rdev::simulate_unicode(unicode as _)); + allow_err!(rdev::simulate_unicode(_unicode as _)); } Some(key_event::Union::Chr(..)) => { From d263d1892bf6fc1258c5b922b8cbd3b0922deebe Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:34:52 +0800 Subject: [PATCH 407/734] update rdev Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 988363019..93b40ca3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4404,7 +4404,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#77b45e9e43f713851874c7fbb8e7149ab4f2e6a1" +source = "git+https://github.com/fufesou/rdev#4d8231f05e14c5a04cd7d2c1288e87ad52d39e4c" dependencies = [ "cocoa", "core-foundation 0.9.3", From 948f9f28dbbd2846e8026f595021d7fb2a7c0b73 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:15:08 +0100 Subject: [PATCH 408/734] Update it.rs --- src/lang/it.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 9730bbc2d..a4ea58304 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Chiamata vocale"), + ("Text chat", "Chat testuale"), + ("Stop voice call", "Interrompi la chiamata vocale"), ].iter().cloned().collect(); } From 7c13be587638c61ffee6fe09a82c2e114ffd2531 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 17:26:44 +0800 Subject: [PATCH 409/734] update issue template and clippy for hbb_common --- .github/ISSUE_TEMPLATE/bug_report.yaml | 15 ++++-- libs/hbb_common/build.rs | 4 +- libs/hbb_common/src/bytes_codec.rs | 40 ++++++++-------- libs/hbb_common/src/config.rs | 6 +-- libs/hbb_common/src/lib.rs | 60 +++++++++++------------- libs/hbb_common/src/password_security.rs | 48 +++++++++---------- libs/hbb_common/src/platform/linux.rs | 2 +- libs/hbb_common/src/protos/mod.rs | 2 +- libs/hbb_common/src/socket_client.rs | 22 ++++----- libs/hbb_common/src/tcp.rs | 2 +- 10 files changed, 103 insertions(+), 98 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 16509a3be..c2d92097c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -30,13 +30,22 @@ body: description: A clear and concise description of what you expected to happen validations: required: true + - type: input + id: os + attributes: + label: Operating system(s) on local side and remote side + description: What operating system(s) do you see this bug on? local side -> remote side. + placeholder: | + Windows 10 -> osx + validations: + required: true - type: input id: version attributes: - label: Operating System(s) and RustDesk Version(s) on local side and remote side - description: What Operatiing System(s) and version(s) of RustDesk do you see this bug on? local side / remote side. + label: RustDesk Version(s) on local side and remote side + description: What RustDesk version(s) do you see this bug on? local side -> remote side. placeholder: | - Windows 10, 1.1.9 / osx 13.1, 1.1.8 + 1.1.9 -> 1.1.8 validations: required: true - type: textarea diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs index fe0d31076..5ebc3a287 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -2,11 +2,11 @@ fn main() { let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap()); std::fs::create_dir_all(&out_dir).unwrap(); - + protobuf_codegen::Codegen::new() .pure() .out_dir(out_dir) - .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) + .inputs(["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) .run() diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs index 699aa9bff..bfc798715 100644 --- a/libs/hbb_common/src/bytes_codec.rs +++ b/libs/hbb_common/src/bytes_codec.rs @@ -143,32 +143,32 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3F, 1); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); let buf_saved = buf.clone(); assert_eq!(buf.len(), 0x3F + 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F); assert_eq!(res[0], 1); } else { - assert!(false); + panic!(); } let mut codec2 = BytesCodec::new(); let mut buf2 = BytesMut::new(); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[0..1]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[1..]); if let Ok(Some(res)) = codec2.decode(&mut buf2) { assert_eq!(res.len(), 0x3F); assert_eq!(res[0], 1); } else { - assert!(false); + panic!(); } } @@ -177,21 +177,21 @@ mod tests { let mut codec = BytesCodec::new(); let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); - assert!(!codec.encode("".into(), &mut buf).is_err()); + assert!(codec.encode("".into(), &mut buf).is_ok()); assert_eq!(buf.len(), 1); bytes.resize(0x3F + 1, 2); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3F + 2 + 2); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0); } else { - assert!(false); + panic!(); } if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F + 1); assert_eq!(res[0], 2); } else { - assert!(false); + panic!(); } } @@ -201,13 +201,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3F - 1, 3); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3F + 1 - 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F - 1); assert_eq!(res[0], 3); } else { - assert!(false); + panic!(); } } #[test] @@ -216,13 +216,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFF, 4); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3FFF + 2); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFF); assert_eq!(res[0], 4); } else { - assert!(false); + panic!(); } } @@ -232,13 +232,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFFFF, 5); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3FFFFF + 3); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFFFF); assert_eq!(res[0], 5); } else { - assert!(false); + panic!(); } } @@ -248,33 +248,33 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFFFF + 1, 6); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); let buf_saved = buf.clone(); assert_eq!(buf.len(), 0x3FFFFF + 4 + 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFFFF + 1); assert_eq!(res[0], 6); } else { - assert!(false); + panic!(); } let mut codec2 = BytesCodec::new(); let mut buf2 = BytesMut::new(); buf2.extend(&buf_saved[0..1]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[1..6]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[6..]); if let Ok(Some(res)) = codec2.decode(&mut buf2) { assert_eq!(res.len(), 0x3FFFFF + 1); assert_eq!(res[0], 6); } else { - assert!(false); + panic!(); } } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 71dd9a5c6..1e4d80c9f 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -288,7 +288,7 @@ fn patch(path: PathBuf) -> PathBuf { .trim() .to_owned(); if user != "root" { - return format!("/home/{}", user).into(); + return format!("/home/{user}").into(); } } } @@ -525,7 +525,7 @@ impl Config { let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); fs::create_dir(&path).ok(); fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); - path.push(format!("ipc{}", postfix)); + path.push(format!("ipc{postfix}")); path.to_str().unwrap_or("").to_owned() } } @@ -562,7 +562,7 @@ impl Config { .unwrap_or_default(); } if !rendezvous_server.contains(':') { - rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT); + rendezvous_server = format!("{rendezvous_server}:{RENDEZVOUS_PORT}"); } rendezvous_server } diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index c9f9e90d7..1c49adfb7 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -211,11 +211,7 @@ pub fn gen_version() { // generate build date let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); file.write_all( - format!( - "#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{}\";", - build_date - ) - .as_bytes(), + format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(), ) .ok(); file.sync_all().ok(); @@ -342,39 +338,39 @@ mod test { #[test] fn test_ipv6() { - assert_eq!(is_ipv6_str("1:2:3"), true); - assert_eq!(is_ipv6_str("[ab:2:3]:12"), true); - assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true); - assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false); - assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false); - assert_eq!(is_ipv6_str("1.1.1.1"), false); - assert_eq!(is_ip_str("1.1.1.1"), true); - assert_eq!(is_ipv6_str("1:2:"), false); - assert_eq!(is_ipv6_str("1:2::0"), true); - assert_eq!(is_ipv6_str("[1:2::0]:1"), true); - assert_eq!(is_ipv6_str("[1:2::0]:"), false); - assert_eq!(is_ipv6_str("1:2::0]:1"), false); + assert!(is_ipv6_str("1:2:3")); + assert!(is_ipv6_str("[ab:2:3]:12")); + assert!(is_ipv6_str("[ABEF:2a:3]:12")); + assert!(!is_ipv6_str("[ABEG:2a:3]:12")); + assert!(!is_ipv6_str("1[ab:2:3]:12")); + assert!(!is_ipv6_str("1.1.1.1")); + assert!(is_ip_str("1.1.1.1")); + assert!(!is_ipv6_str("1:2:")); + assert!(is_ipv6_str("1:2::0")); + assert!(is_ipv6_str("[1:2::0]:1")); + assert!(!is_ipv6_str("[1:2::0]:")); + assert!(!is_ipv6_str("1:2::0]:1")); } #[test] fn test_hostname_port() { - assert_eq!(is_domain_port_str("a:12"), false); - assert_eq!(is_domain_port_str("a.b.c:12"), false); - assert_eq!(is_domain_port_str("test.com:12"), true); - assert_eq!(is_domain_port_str("test-UPPER.com:12"), true); - assert_eq!(is_domain_port_str("some-other.domain.com:12"), true); - assert_eq!(is_domain_port_str("under_score:12"), false); - assert_eq!(is_domain_port_str("a@bc:12"), false); - assert_eq!(is_domain_port_str("1.1.1.1:12"), false); - assert_eq!(is_domain_port_str("1.2.3:12"), false); - assert_eq!(is_domain_port_str("1.2.3.45:12"), false); - assert_eq!(is_domain_port_str("a.b.c:123456"), false); - assert_eq!(is_domain_port_str("---:12"), false); - assert_eq!(is_domain_port_str(".:12"), false); + assert!(!is_domain_port_str("a:12")); + assert!(!is_domain_port_str("a.b.c:12")); + assert!(is_domain_port_str("test.com:12")); + assert!(is_domain_port_str("test-UPPER.com:12")); + assert!(is_domain_port_str("some-other.domain.com:12")); + assert!(!is_domain_port_str("under_score:12")); + assert!(!is_domain_port_str("a@bc:12")); + assert!(!is_domain_port_str("1.1.1.1:12")); + assert!(!is_domain_port_str("1.2.3:12")); + assert!(!is_domain_port_str("1.2.3.45:12")); + assert!(!is_domain_port_str("a.b.c:123456")); + assert!(!is_domain_port_str("---:12")); + assert!(!is_domain_port_str(".:12")); // todo: should we also check for these edge cases? // out-of-range port - assert_eq!(is_domain_port_str("test.com:0"), true); - assert_eq!(is_domain_port_str("test.com:98989"), true); + assert!(is_domain_port_str("test.com:0")); + assert!(is_domain_port_str("test.com:98989")); } #[test] diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 0b66107fc..ddfe28baa 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -192,51 +192,51 @@ mod test { let data = "Hello World"; let encrypted = encrypt_str_or_original(data, version); let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); - println!("data: {}", data); - println!("encrypted: {}", encrypted); - println!("decrypted: {}", decrypted); + println!("data: {data}"); + println!("encrypted: {encrypted}"); + println!("decrypted: {decrypted}"); assert_eq!(data, decrypted); assert_eq!(version, &encrypted[..2]); - assert_eq!(succ, true); - assert_eq!(store, false); + assert!(succ); + assert!(!store); let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); - assert_eq!(store, true); - assert_eq!(decrypt_str_or_original(&decrypted, version).1, false); + assert!(store); + assert!(!decrypt_str_or_original(&decrypted, version).1); assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted); println!("test vec"); let data: Vec = vec![1, 2, 3, 4, 5, 6]; let encrypted = encrypt_vec_or_original(&data, version); let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); - println!("data: {:?}", data); - println!("encrypted: {:?}", encrypted); - println!("decrypted: {:?}", decrypted); + println!("data: {data:?}"); + println!("encrypted: {encrypted:?}"); + println!("decrypted: {decrypted:?}"); assert_eq!(data, decrypted); assert_eq!(version.as_bytes(), &encrypted[..2]); - assert_eq!(store, false); - assert_eq!(succ, true); + assert!(!store); + assert!(succ); let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); - assert_eq!(store, true); - assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false); + assert!(store); + assert!(!decrypt_vec_or_original(&decrypted, version).1); assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted); println!("test original"); let data = version.to_string() + "Hello World"; let (decrypted, succ, store) = decrypt_str_or_original(&data, version); assert_eq!(data, decrypted); - assert_eq!(store, true); - assert_eq!(succ, false); + assert!(store); + assert!(!succ); let verbytes = version.as_bytes(); - let data: Vec = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6]; + let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6]; let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); assert_eq!(data, decrypted); - assert_eq!(store, true); - assert_eq!(succ, false); + assert!(store); + assert!(!succ); let (_, succ, store) = decrypt_str_or_original("", version); - assert_eq!(store, false); - assert_eq!(succ, false); - let (_, succ, store) = decrypt_vec_or_original(&vec![], version); - assert_eq!(store, false); - assert_eq!(succ, false); + assert!(!store); + assert!(!succ); + let (_, succ, store) = decrypt_vec_or_original(&[], version); + assert!(!store); + assert!(!succ); } } diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 716025dc7..7c107d11c 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -60,7 +60,7 @@ fn get_display_server_of_session(session: &str) -> String { .replace("TTY=", "") .trim_end() .into(); - if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty)) + if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{tty}.\\\\+Xorg\"")) // And check if Xorg is running on that tty { if xorg_results.trim_end() != "" { diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs index c001c58fb..57d9b68fe 100644 --- a/libs/hbb_common/src/protos/mod.rs +++ b/libs/hbb_common/src/protos/mod.rs @@ -1 +1 @@ -include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); \ No newline at end of file +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index a034b4e12..2d9b5a984 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -13,22 +13,22 @@ use tokio_socks::{IntoTargetAddr, TargetAddr}; pub fn check_port(host: T, port: i32) -> String { let host = host.to_string(); if crate::is_ipv6_str(&host) { - if host.starts_with("[") { + if host.starts_with('[') { return host; } - return format!("[{}]:{}", host, port); + return format!("[{host}]:{port}"); } - if !host.contains(":") { - return format!("{}:{}", host, port); + if !host.contains(':') { + return format!("{host}:{port}"); } - return host; + host } #[inline] pub fn increase_port(host: T, offset: i32) -> String { let host = host.to_string(); if crate::is_ipv6_str(&host) { - if host.starts_with("[") { + if host.starts_with('[') { let tmp: Vec<&str> = host.split("]:").collect(); if tmp.len() == 2 { let port: i32 = tmp[1].parse().unwrap_or(0); @@ -37,8 +37,8 @@ pub fn increase_port(host: T, offset: i32) -> String { } } } - } else if host.contains(":") { - let tmp: Vec<&str> = host.split(":").collect(); + } else if host.contains(':') { + let tmp: Vec<&str> = host.split(':').collect(); if tmp.len() == 2 { let port: i32 = tmp[1].parse().unwrap_or(0); if port > 0 { @@ -46,7 +46,7 @@ pub fn increase_port(host: T, offset: i32) -> String { } } } - return host; + host } pub fn test_if_valid_server(host: &str) -> String { @@ -148,7 +148,7 @@ pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { if !ipv4 && crate::is_ipv4_str(&addr) { if let Some(ip) = addr.split(':').next() { - return addr.replace(ip, &format!("{}.nip.io", ip)); + return addr.replace(ip, &format!("{ip}.nip.io")); } } addr @@ -163,7 +163,7 @@ async fn test_target(target: &str) -> ResultType { tokio::net::lookup_host(target) .await? .next() - .context(format!("Failed to look up host for {}", target)) + .context(format!("Failed to look up host for {target}")) } #[inline] diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index a7ac4eb3a..f574e8309 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -100,7 +100,7 @@ impl FramedStream { } } } - bail!(format!("Failed to connect to {}", remote_addr)); + bail!(format!("Failed to connect to {remote_addr}")); } pub async fn connect<'a, 't, P, T>( From 4134b77680126f408e5ce88f7eb4c3ad5711b749 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 19:17:59 +0800 Subject: [PATCH 410/734] improve ffi enum data size, fix compile warning on mac --- src/client/io_loop.rs | 2 +- src/common.rs | 7 +---- src/core_main.rs | 4 +-- src/ipc.rs | 28 +++++++++++++++---- src/keyboard.rs | 6 ++-- src/server.rs | 62 +++++++++++++++++++++--------------------- src/ui/macos.rs | 9 ++---- src/ui_cm_interface.rs | 4 ++- 8 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f5792bce3..5186aff4d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -25,7 +25,7 @@ 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, LoginConfigHandler, MediaData, MediaSender, + new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; diff --git a/src/common.rs b/src/common.rs index 2142d973d..79a4664db 100644 --- a/src/common.rs +++ b/src/common.rs @@ -30,7 +30,7 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; -use crate::ui_interface::{set_option, get_option}; +use crate::ui_interface::{get_option, set_option}; pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; @@ -762,8 +762,3 @@ 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()) } - -#[cfg(test)] -mod test_common { - -} diff --git a/src/core_main.rs b/src/core_main.rs index 03d057eff..0af7026e9 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,6 +1,4 @@ -use std::future::Future; - -use hbb_common::{log, ResultType}; +use hbb_common::log; /// shared by flutter and sciter main function /// diff --git a/src/ipc.rs b/src/ipc.rs index 0ede560fc..699b0bcd7 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -16,10 +16,10 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, ResultType, timeout, - tokio, + log, password_security as password, timeout, tokio, tokio::io::{AsyncRead, AsyncWrite}, tokio_util::codec::Framed, + ResultType, }; use crate::rendezvous_mediator::RendezvousMediator; @@ -190,7 +190,7 @@ pub enum Data { Socks(Option), FS(FS), Test, - SyncConfig(Option<(Config, Config2)>), + SyncConfig(Option>), #[cfg(not(any(target_os = "android", target_os = "ios")))] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), @@ -419,7 +419,8 @@ async fn handle(data: Data, stream: &mut Connection) { let t = Config::get_nat_type(); allow_err!(stream.send(&Data::NatType(Some(t))).await); } - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = CheckIfRestart::new(); Config::set(config); Config2::set(config2); @@ -428,7 +429,9 @@ async fn handle(data: Data, stream: &mut Connection) { Data::SyncConfig(None) => { allow_err!( stream - .send(&Data::SyncConfig(Some((Config::get(), Config2::get())))) + .send(&Data::SyncConfig(Some( + (Config::get(), Config2::get()).into() + ))) .await ); } @@ -840,6 +843,19 @@ pub async fn test_rendezvous_server() -> ResultType<()> { #[tokio::main(flavor = "current_thread")] pub async fn send_url_scheme(url: String) -> ResultType<()> { - connect(1_000, "_url").await?.send(&Data::UrlLink(url)).await?; + connect(1_000, "_url") + .await? + .send(&Data::UrlLink(url)) + .await?; Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn verify_ffi_enum_data_size() { + println!("{}", std::mem::size_of::()); + assert!(std::mem::size_of::() < 96); + } +} diff --git a/src/keyboard.rs b/src/keyboard.rs index 91480ba30..17c52abf7 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -212,7 +212,7 @@ pub fn start_grab_loop() { } let mut _keyboard_mode = KeyboardMode::Map; - let scan_code = event.scan_code; + let _scan_code = event.scan_code; let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { _keyboard_mode = client::process_event(&event, None); if is_press { @@ -225,7 +225,7 @@ pub fn start_grab_loop() { }; #[cfg(target_os = "windows")] - match scan_code { + match _scan_code { 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), @@ -241,7 +241,7 @@ pub fn start_grab_loop() { #[cfg(target_os = "windows")] unsafe { // AltGr - if scan_code == 0x021D { + if _scan_code == 0x021D { IS_0X021D_DOWN = is_press; } } diff --git a/src/server.rs b/src/server.rs index 616d92375..7807c4fac 100644 --- a/src/server.rs +++ b/src/server.rs @@ -8,6 +8,9 @@ use std::{ use bytes::Bytes; pub use connection::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::config::Config2; +use hbb_common::tcp::new_listener; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -17,18 +20,15 @@ use hbb_common::{ message_proto::*, protobuf::{Enum, Message as _}, rendezvous_proto::*, - ResultType, socket_client, - sodiumoxide::crypto::{box_, secretbox, sign}, Stream, timeout, tokio, + sodiumoxide::crypto::{box_, secretbox, sign}, + timeout, tokio, ResultType, Stream, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::config::Config2; -use hbb_common::tcp::new_listener; -use service::{GenericService, Service, Subscriber}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; +use service::{GenericService, Service, Subscriber}; -use crate::ipc::{connect, Data}; +use crate::ipc::Data; pub mod audio_service; cfg_if::cfg_if! { @@ -65,7 +65,7 @@ type ConnMap = HashMap; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CONN_COUNT: Arc> = Default::default(); - // A client server used to provide local services(audio, video, clipboard, etc.) + // A client server used to provide local services(audio, video, clipboard, etc.) // for all initiative connections. // // [Note] @@ -420,7 +420,8 @@ pub async fn start_server(is_server: bool) { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; if Config::set(config) { log::info!("config synced"); } @@ -450,28 +451,26 @@ pub async fn start_ipc_url_server() { while let Some(Ok(conn)) = incoming.next().await { let mut conn = crate::ipc::Connection::new(conn); match conn.next_timeout(1000).await { - Ok(Some(data)) => { - match data { - Data::UrlLink(url) => { - #[cfg(feature = "flutter")] - { - if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM.read().unwrap().get( - crate::flutter::APP_TYPE_MAIN - ) { - let mut m = HashMap::new(); - m.insert("name", "on_url_scheme_received"); - m.insert("url", url.as_str()); - stream.add(serde_json::to_string(&m).unwrap()); - } else { - log::warn!("No main window app found!"); - } - } - } - _ => { - log::warn!("An unexpected data was sent to the ipc url server.") + Ok(Some(data)) => match data { + #[cfg(feature = "flutter")] + Data::UrlLink(url) => { + if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(crate::flutter::APP_TYPE_MAIN) + { + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + stream.add(serde_json::to_string(&m).unwrap()); + } else { + log::warn!("No main window app found!"); } } - } + _ => { + log::warn!("An unexpected data was sent to the ipc url server.") + } + }, Err(err) => { log::error!("{}", err); } @@ -509,7 +508,8 @@ async fn sync_and_watch_config_dir() { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = crate::ipc::CheckIfRestart::new(); if cfg0.0 != config { cfg0.0 = config.clone(); @@ -534,7 +534,7 @@ async fn sync_and_watch_config_dir() { let cfg = (Config::get(), Config2::get()); if cfg != cfg0 { log::info!("config updated, sync to root"); - match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await { + match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await { Err(e) => { log::error!("sync config to root failed: {}", e); break; diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 98e355dc1..f34b7c2c1 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -14,12 +14,9 @@ use objc::{ sel, sel_impl, }; use objc::runtime::Class; -use objc_id::WeakId; use sciter::{Host, make_args}; -use hbb_common::{log, tokio}; - -use crate::ui_cm_interface::start_ipc; +use hbb_common::log; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -141,7 +138,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - hbb_common::log::debug!("icon clicked on finder"); + log::debug!("icon clicked on finder"); if std::env::args().nth(1) == Some("--server".to_owned()) { crate::platform::macos::check_main_window(); } @@ -267,4 +264,4 @@ pub fn make_tray() { set_delegate(None); } crate::tray::make_tray(); -} \ No newline at end of file +} diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index ccddab0ee..de33b0169 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -845,6 +845,7 @@ pub fn elevate_portable(_id: i32) { } } +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn handle_incoming_voice_call(id: i32, accept: bool) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { @@ -852,9 +853,10 @@ pub fn handle_incoming_voice_call(id: i32, accept: bool) { }; } +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn close_voice_call(id: i32) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); }; -} \ No newline at end of file +} From 1588e44d61b255ed3eb99529e90dd7e18e4779d9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:17:43 +0800 Subject: [PATCH 411/734] win, translate mode, fix dead key Signed-off-by: fufesou --- Cargo.lock | 2 +- src/keyboard.rs | 21 +++++++++++++-------- src/ui_session_interface.rs | 6 +++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c53c573f2..83f623ca7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4405,7 +4405,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#4d8231f05e14c5a04cd7d2c1288e87ad52d39e4c" +source = "git+https://github.com/fufesou/rdev#cedc4e62744566775026af4b434ef799804c1130" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/keyboard.rs b/src/keyboard.rs index 17c52abf7..5b9920714 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -193,8 +193,8 @@ pub mod client { #[cfg(windows)] pub fn update_grab_get_key_name() { match get_keyboard_mode_enum() { - KeyboardMode::Map => rdev::set_get_key_name(false), - KeyboardMode::Translate => rdev::set_get_key_name(true), + KeyboardMode::Map => rdev::set_get_key_unicode(false), + KeyboardMode::Translate => rdev::set_get_key_unicode(true), _ => {} }; } @@ -256,6 +256,7 @@ pub fn start_grab_loop() { if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } + rdev::set_event_popup(false); }); #[cfg(target_os = "linux")] @@ -757,12 +758,10 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - if !unicode_info.is_dead { - for code in &unicode_info.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*code as _); - events.push(evt); - } + for code in &unicode_info.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*code as _); + events.push(evt); } } None => {} @@ -816,6 +815,12 @@ pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Opti pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { let mut events: Vec = Vec::new(); + if let Some(unicode_info) = &event.unicode { + if unicode_info.is_dead { + return events; + } + } + #[cfg(target_os = "windows")] unsafe { if event.scan_code == 0x021D { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index dc0e365ab..87ea8e9eb 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -368,8 +368,8 @@ impl Session { #[cfg(target_os = "windows")] { match &self.lc.read().unwrap().keyboard_mode as _ { - "legacy" => rdev::set_get_key_name(true), - "translate" => rdev::set_get_key_name(true), + "legacy" => rdev::set_get_key_unicode(true), + "translate" => rdev::set_get_key_unicode(true), _ => {} } } @@ -381,7 +381,7 @@ impl Session { pub fn leave(&self) { #[cfg(target_os = "windows")] { - rdev::set_get_key_name(false); + rdev::set_get_key_unicode(false); } IS_IN.store(false, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Wait); From c049e728fd3f49db3b99338c377131294ce90473 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:25:25 +0800 Subject: [PATCH 412/734] suppress warns Signed-off-by: fufesou --- src/flutter_ffi.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2e6c450c1..bb1b8b8b9 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -928,7 +928,7 @@ pub fn main_start_dbus_server() { { use crate::dbus::start_dbus_server; // spawn new thread to start dbus server - std::thread::spawn(|| { + thread::spawn(|| { let _ = start_dbus_server(); }); } @@ -1275,7 +1275,7 @@ pub fn main_is_login_wayland() -> SyncReturn { pub fn main_start_pa() { #[cfg(target_os = "linux")] - std::thread::spawn(crate::ipc::start_pa); + thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { @@ -1302,9 +1302,9 @@ pub fn main_start_ipc_url_server() { /// /// * macOS only #[allow(unused_variables)] -pub fn send_url_scheme(url: String) { +pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); + thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); } #[cfg(target_os = "android")] @@ -1324,7 +1324,7 @@ pub mod server_side { _class: JClass, ) { log::debug!("startServer from java"); - std::thread::spawn(move || start_server(true)); + thread::spawn(move || start_server(true)); } #[no_mangle] From 3a0137a3f71bef19fa3a0e440f3025c860cfcaf3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:45:15 +0800 Subject: [PATCH 413/734] suppress warns Signed-off-by: fufesou --- src/flutter_ffi.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index bb1b8b8b9..ec4a90973 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,9 @@ -use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char, thread}; +use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; use std::str::FromStr; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::thread; + use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; From 2feed1cdaf26c0f1a0f4f07819bfcb86b0e3934e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 20:00:16 +0800 Subject: [PATCH 414/734] though this change exe name to rustdesk, but it also change the name used in the other place --- flutter/macos/Runner.xcodeproj/project.pbxproj | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 18166c8ff..066560203 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* rustdesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = rustdesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* RustDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RustDesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -127,7 +127,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* rustdesk.app */, + 33CC10ED2044A3C60003C045 /* RustDesk.app */, ); name = Products; sourceTree = ""; @@ -212,7 +212,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */; + productReference = 33CC10ED2044A3C60003C045 /* RustDesk.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -462,7 +462,6 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -608,7 +607,6 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; @@ -646,7 +644,6 @@ /dev/null, ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; From 80da209be8226a0af25b64511e6c35f36cfb8829 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 20:09:07 +0800 Subject: [PATCH 415/734] change executable name from RustDesk to rustdesk in mac deployment --- .github/workflows/flutter-nightly.yml | 1 - build.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 5ca284cee..f03cd0be8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -242,7 +242,6 @@ jobs: security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain # start sign the rustdesk.app and dmg rm rustdesk-${{ env.VERSION }}.dmg || true - mv ./flutter/build/macos/Build/Products/Release/rustdesk.app ./flutter/build/macos/Build/Products/Release/RustDesk.app codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v 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 diff --git a/build.py b/build.py index 6b107ff4b..dce434720 100755 --- a/build.py +++ b/build.py @@ -322,8 +322,9 @@ 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") + "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.chdir("..") From 3ae53a5d577b944baffb33da4ef9c959fefa1c72 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 20:41:19 +0800 Subject: [PATCH 416/734] fix build Signed-off-by: fufesou --- src/keyboard.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keyboard.rs b/src/keyboard.rs index 5b9920714..28e151580 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -256,6 +256,7 @@ pub fn start_grab_loop() { if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } + #[cfg(target_os = "windows")] rdev::set_event_popup(false); }); From 4bd4fba533ea31c773342e279e7ac1c22b39a499 Mon Sep 17 00:00:00 2001 From: sjpark Date: Wed, 8 Feb 2023 21:45:10 +0900 Subject: [PATCH 417/734] allow swap key --- .../desktop/pages/desktop_setting_page.dart | 2 + memo.txt | 104 ++++++++++++++++++ src/flutter.rs | 2 + src/keyboard.rs | 47 ++++++++ src/server/connection.rs | 60 ++++++++++ src/ui/index.tis | 1 + src/ui/remote.rs | 2 + src/ui_session_interface.rs | 10 ++ 8 files changed, 228 insertions(+) create mode 100644 memo.txt diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4b6cf2a62..67eb0234b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -278,6 +278,8 @@ class _GeneralState extends State<_General> { _OptionCheckBox(context, 'Confirm before closing multiple tabs', 'enable-confirm-closing-tabs'), _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), + if (Platform.isMacOS) + _OptionCheckBox(context, 'Swap control-command key', 'allow-swap-key'), if (Platform.isLinux) Tooltip( message: translate('software_render_tip'), diff --git a/memo.txt b/memo.txt new file mode 100644 index 000000000..da23ae423 --- /dev/null +++ b/memo.txt @@ -0,0 +1,104 @@ +#windows +python3 res/inline-sciter.py +cargo build --release --features inline,with_rc --target=aarch64-pc-windows-msvc -vv + +Push-Location flutter ; flutter pub get ; Pop-Location +~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + +%comspec% /k "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsamd64_arm64.bat" +rustup update +rustup target add aarch64-pc-windows-msvc +rustup target list + +reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug" /v Auto /t REG_DWORD /d 1 /f +reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\rustdesk.exe" /v Debugger /t REG_SZ /d "vsjitdebugger.exe" /f + +#macos +pushd flutter && flutter pub get && popd +~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart +./build.py --flutter +codesign --force --options runtime -s "Developer ID Application" --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v +rm -r /Applications/RustDesk.app +cp -r ./flutter/build/macos/Build/Products/Release/RustDesk.app /Applications/RustDesk.app +open -n /Applications/RustDesk.app --args --server + +cargo bundle --release --features inline +cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS +mv target/release/bundle/osx/RustDesk.app/Contents/Resources/res/* target/release/bundle/osx/RustDesk.app/Contents/Resources +rm -rf target/release/bundle/osx/RustDesk.app/Contents/Resources/res +target/release/bundle/osx/RustDesk.app/Contents/Info.plist + LSUIElement + 1 + +python3 res/inline-sciter.py +cargo build --release --features inline +cp target/release/rustdesk ../Documents/RustDesk.app/Contents/MacOS/rustdesk +codesign -s "Developer ID Application" --force --options runtime ../Documents/RustDesk.app/Contents/MacOS/* +codesign -s "Developer ID Application" --force --options runtime ../Documents/RustDesk.app +rm -r /Applications/RustDesk.app +cp -r ../Documents/RustDesk.app /Applications/RustDesk.app + +csrutil disable +file target\release\rustdesk +sudo lsof -i -n -P | grep rustdesk // netstat +https://github.com/create-dmg/create-dmg +security find-identity -p basic -v + +#android +BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android="--target=arm64-apple-macos --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" RUST_LOG=debug cargo ndk --platform 21 --target aarch64-linux-android rustc --lib --features flutter --release +cp target/aarch64-linux-android/release/liblibrustdesk.so flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so +pushd flutter; flutter build apk --target-platform android-arm64 --release; popd +adb install flutter/build/app/outputs/flutter-apk/app-release.apk + +sudo mount -t drvfs '\\192.168.111.10\Macintosh HD' /mnt/mac +cp target/aarch64-linux-android/debug/liblibrustdesk.so /mnt/mac/Users/sjpark/rustdesk/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + +adb logcat | grep LOG_SERVICE +adb emu kill + +sudo apt install build-essential +sudo apt install gcc-multilib + +#ios +flutter/ios/Runnder.xcworkspace/View/Navigators/Project/targets:Runner/Signing & Capability/Teams +cargo build --target aarch64-apple-ios --features flutter --release +pushd flutter; flutter build ios --release; popd +xcode/Window/Devices and Simulators/INSTALLED APPS + +xcrun simctl list +open -a Simulator --args -CurrentDeviceUDID 5D1C39DD-708B-41D3-B89A-3F0D9B8E42BF + +# rustdesk +cd C:\Users\sjpark\Documents\rustdesk +set VCPKG_ROOT=C:\Users\sjpark\Documents\vcpkg +set LIBCLANG_PATH=C:\Program Files\LLVM\bin + +# ring +set path=C:\Program Files\LLVM\bin;C:\Strawberry\perl\bin\;%path%; +.\target\tools\windows\nasm\nasm.exe +set RING_PREGENERATE_ASM=1 + +#dependencies +ring = { git = "https://github.com/sj6219/ring", branch = "0.16.20_alpha" } + + +adb shell dumpsys package com.carriez.flutter_hbb +objdump -T ~/rustdesk/target/aarch64-linux-android/release/liblibrustdesk.so + +cat /Users/sjpark/Library/Android/sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/11.0.5/lib/linux/aarch64/lldb-server | adb shell sh -c 'cat > /data/local/tmp/lldb-server && chmod 755 /data/local/tmp/lldb-server' +adb shell run-as com.carriez.flutter_hbb mkdir -p /data/data/com.carriez.flutter_hbb/lldb/bin/ +adb shell "cat /data/local/tmp/lldb-server | run-as com.carriez.flutter_hbb sh -c 'cat > /data/data/com.carriez.flutter_hbb/lldb/bin/lldb-server && chmod 755 /data/data/com.carriez.flutter_hbb/lldb/bin/lldb-server'" + +adb shell ps -e -o PID -o NAME | grep com.carriez.flutter_hbb +adb forward tcp:10086 tcp:10086 +adb shell run-as com.carriez.flutter_hbb /data/data/com.carriez.flutter_hbb/lldb/bin/lldb-server platform --listen "*:10086" --server + +/Users/sjpark/Library/Android/sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/darwin-x86_64/bin/lldb +platform select remote-android +platform connect connect://localhost:10086 +attach +b connection.rs:624 + +add-dsym /Users/sjpark/ndk-samples/hello-gl2/app/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libgl2jni.so +b gl_code.cpp:151 + diff --git a/src/flutter.rs b/src/flutter.rs index 2d7d3fb86..bee585d93 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -428,8 +428,10 @@ pub fn session_add( let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); + let allow_swap_key = hbb_common::config::Config::get_option("allow-swap-key") == "Y"; let session: Session = Session { id: session_id.clone(), + allow_swap_key, ..Default::default() }; diff --git a/src/keyboard.rs b/src/keyboard.rs index 91480ba30..2764a4408 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -210,6 +210,53 @@ pub fn start_grab_loop() { if key == Key::CapsLock || key == Key::NumLock { return Some(event); } + #[cfg(target_os = "macos")] + let mut event = event; + #[cfg(target_os = "macos")] { + let mut allow_swap_key = false; + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + allow_swap_key = session.allow_swap_key; + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + allow_swap_key = session.allow_swap_key; + } + if allow_swap_key { + match event.event_type { + EventType::KeyPress( key) => { + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + event.event_type = EventType::KeyPress(key); + event.scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); + event.code = event.scan_code as _; + } + EventType::KeyRelease(key) => { + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + event.event_type = EventType::KeyRelease(key); + event.scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); + event.code = event.scan_code as _; + } + _ => {} + }; + }; + }; + let mut _keyboard_mode = KeyboardMode::Map; let scan_code = event.scan_code; diff --git a/src/server/connection.rs b/src/server/connection.rs index 9ce53c960..17d4e3768 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -539,6 +539,9 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { + #[cfg(target_os = "macos")] + let allow_swap_key = hbb_common::config::Config::get_option("allow-swap-key") == "Y"; + let mut block_input_mode = false; #[cfg(target_os = "windows")] { @@ -551,9 +554,66 @@ impl Connection { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { Ok(v) => match v { MessageInput::Mouse((msg, id)) => { + #[cfg(target_os = "macos")] + let msg = { + let mut msg = msg; + if allow_swap_key { + msg.modifiers = msg.modifiers.iter().map(|ck| { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + hbb_common::protobuf::EnumOrUnknown::new(ck) + }).collect(); + } + msg + }; + handle_mouse(&msg, id); } MessageInput::Key((mut msg, press)) => { + #[cfg(target_os = "macos")] + if allow_swap_key { + if let Some(key_event::Union::ControlKey(ck)) = msg.union { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + msg.set_control_key(ck); + } + msg.modifiers = msg.modifiers.iter().map(|ck| { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + hbb_common::protobuf::EnumOrUnknown::new(ck) + }).collect(); + + let code = msg.chr(); + if code != 0 { + let key = rdev::key_from_code(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + msg.set_chr(rdev::macos_keycode_from_key(key).unwrap_or_default()); + } + } // todo: press and down have similar meanings. if press && msg.mode.unwrap() == KeyboardMode::Legacy { msg.down = true; diff --git a/src/ui/index.tis b/src/ui/index.tis index ec2e0a748..20228ea03 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -214,6 +214,7 @@ class Enhancements: Reactor.Component { {has_hwcodec ?

  • {svg_checkmark}{translate("Hardware Codec")} (beta)
  • : ""}
  • {svg_checkmark}{translate("Adaptive Bitrate")} (beta)
  • {translate("Recording")}
  • + {is_osx ?
  • {svg_checkmark}{translate("Swap control-command key")}
  • : "" } ; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 999b409e0..2d0d4d2c0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -443,10 +443,12 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { + let allow_swap_key = hbb_common::config::Config::get_option("allow-swap-key") == "Y"; let session: Session = Session { id: id.clone(), password: password.clone(), args, + allow_swap_key, ..Default::default() }; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index dc0e365ab..a0c4f06b0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -36,6 +36,7 @@ pub struct Session { pub sender: Arc>>>, pub thread: Arc>>>, pub ui_handler: T, + pub allow_swap_key: bool, } impl Session { @@ -505,6 +506,15 @@ impl Session { shift: bool, command: bool, ) { + #[cfg(target_os = "macos")] + let (ctrl, command) = + if self.allow_swap_key { + (command, ctrl) + } + else { + (ctrl, command) + }; + #[allow(unused_mut)] let mut command = command; #[cfg(windows)] From 4261e988d2c4e37b2d8452d1ea623aeb81fd9d15 Mon Sep 17 00:00:00 2001 From: sjpark Date: Wed, 8 Feb 2023 21:49:53 +0900 Subject: [PATCH 418/734] delete memo --- memo.txt | 104 ------------------------------------------------------- 1 file changed, 104 deletions(-) delete mode 100644 memo.txt diff --git a/memo.txt b/memo.txt deleted file mode 100644 index da23ae423..000000000 --- a/memo.txt +++ /dev/null @@ -1,104 +0,0 @@ -#windows -python3 res/inline-sciter.py -cargo build --release --features inline,with_rc --target=aarch64-pc-windows-msvc -vv - -Push-Location flutter ; flutter pub get ; Pop-Location -~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - -%comspec% /k "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsamd64_arm64.bat" -rustup update -rustup target add aarch64-pc-windows-msvc -rustup target list - -reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug" /v Auto /t REG_DWORD /d 1 /f -reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\rustdesk.exe" /v Debugger /t REG_SZ /d "vsjitdebugger.exe" /f - -#macos -pushd flutter && flutter pub get && popd -~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart -./build.py --flutter -codesign --force --options runtime -s "Developer ID Application" --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v -rm -r /Applications/RustDesk.app -cp -r ./flutter/build/macos/Build/Products/Release/RustDesk.app /Applications/RustDesk.app -open -n /Applications/RustDesk.app --args --server - -cargo bundle --release --features inline -cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS -mv target/release/bundle/osx/RustDesk.app/Contents/Resources/res/* target/release/bundle/osx/RustDesk.app/Contents/Resources -rm -rf target/release/bundle/osx/RustDesk.app/Contents/Resources/res -target/release/bundle/osx/RustDesk.app/Contents/Info.plist - LSUIElement - 1 - -python3 res/inline-sciter.py -cargo build --release --features inline -cp target/release/rustdesk ../Documents/RustDesk.app/Contents/MacOS/rustdesk -codesign -s "Developer ID Application" --force --options runtime ../Documents/RustDesk.app/Contents/MacOS/* -codesign -s "Developer ID Application" --force --options runtime ../Documents/RustDesk.app -rm -r /Applications/RustDesk.app -cp -r ../Documents/RustDesk.app /Applications/RustDesk.app - -csrutil disable -file target\release\rustdesk -sudo lsof -i -n -P | grep rustdesk // netstat -https://github.com/create-dmg/create-dmg -security find-identity -p basic -v - -#android -BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android="--target=arm64-apple-macos --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" RUST_LOG=debug cargo ndk --platform 21 --target aarch64-linux-android rustc --lib --features flutter --release -cp target/aarch64-linux-android/release/liblibrustdesk.so flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so -pushd flutter; flutter build apk --target-platform android-arm64 --release; popd -adb install flutter/build/app/outputs/flutter-apk/app-release.apk - -sudo mount -t drvfs '\\192.168.111.10\Macintosh HD' /mnt/mac -cp target/aarch64-linux-android/debug/liblibrustdesk.so /mnt/mac/Users/sjpark/rustdesk/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - -adb logcat | grep LOG_SERVICE -adb emu kill - -sudo apt install build-essential -sudo apt install gcc-multilib - -#ios -flutter/ios/Runnder.xcworkspace/View/Navigators/Project/targets:Runner/Signing & Capability/Teams -cargo build --target aarch64-apple-ios --features flutter --release -pushd flutter; flutter build ios --release; popd -xcode/Window/Devices and Simulators/INSTALLED APPS - -xcrun simctl list -open -a Simulator --args -CurrentDeviceUDID 5D1C39DD-708B-41D3-B89A-3F0D9B8E42BF - -# rustdesk -cd C:\Users\sjpark\Documents\rustdesk -set VCPKG_ROOT=C:\Users\sjpark\Documents\vcpkg -set LIBCLANG_PATH=C:\Program Files\LLVM\bin - -# ring -set path=C:\Program Files\LLVM\bin;C:\Strawberry\perl\bin\;%path%; -.\target\tools\windows\nasm\nasm.exe -set RING_PREGENERATE_ASM=1 - -#dependencies -ring = { git = "https://github.com/sj6219/ring", branch = "0.16.20_alpha" } - - -adb shell dumpsys package com.carriez.flutter_hbb -objdump -T ~/rustdesk/target/aarch64-linux-android/release/liblibrustdesk.so - -cat /Users/sjpark/Library/Android/sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/11.0.5/lib/linux/aarch64/lldb-server | adb shell sh -c 'cat > /data/local/tmp/lldb-server && chmod 755 /data/local/tmp/lldb-server' -adb shell run-as com.carriez.flutter_hbb mkdir -p /data/data/com.carriez.flutter_hbb/lldb/bin/ -adb shell "cat /data/local/tmp/lldb-server | run-as com.carriez.flutter_hbb sh -c 'cat > /data/data/com.carriez.flutter_hbb/lldb/bin/lldb-server && chmod 755 /data/data/com.carriez.flutter_hbb/lldb/bin/lldb-server'" - -adb shell ps -e -o PID -o NAME | grep com.carriez.flutter_hbb -adb forward tcp:10086 tcp:10086 -adb shell run-as com.carriez.flutter_hbb /data/data/com.carriez.flutter_hbb/lldb/bin/lldb-server platform --listen "*:10086" --server - -/Users/sjpark/Library/Android/sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/darwin-x86_64/bin/lldb -platform select remote-android -platform connect connect://localhost:10086 -attach -b connection.rs:624 - -add-dsym /Users/sjpark/ndk-samples/hello-gl2/app/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libgl2jni.so -b gl_code.cpp:151 - From 0dba0130893b54951fe3df3b9ce4997037a0a7ca Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 21:54:48 +0900 Subject: [PATCH 419/734] remove unused Overlay in desktop_tab_page.dart and server_page.dart --- .../lib/desktop/pages/desktop_tab_page.dart | 28 +++++++--------- flutter/lib/desktop/pages/server_page.dart | 33 ++++++++----------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index c1965921c..35d5a61ef 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -64,23 +64,17 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { final tabWidget = Container( - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: DesktopTab( - controller: tabController, - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - onTap: DesktopTabPage.onAddSetting, - isClose: false, - ), - )); - }) - ]), - ); + child: Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: DesktopTab( + controller: tabController, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + onTap: DesktopTabPage.onAddSetting, + isClose: false, + ), + ))); return Platform.isMacOS ? tabWidget : Obx( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 521413647..b4d7f4fac 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -68,26 +68,19 @@ class _DesktopServerPageState extends State ], child: Consumer( builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - ], - ), - ), - ); - }) - ]), - ))); + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + ], + ), + ), + )))); } @override From 3d5aca18d690235ec1fb361b8526a25af7d46672 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 22:01:15 +0900 Subject: [PATCH 420/734] refactor OverlayKeyState for OverlayDialogManager and ChatModel --- flutter/lib/common.dart | 30 +++++++++++-------- flutter/lib/common/widgets/overlay.dart | 24 ++++----------- .../lib/desktop/pages/file_manager_page.dart | 7 +++-- flutter/lib/desktop/pages/remote_page.dart | 16 ++++++---- flutter/lib/models/chat_model.dart | 18 +++++------ 5 files changed, 47 insertions(+), 48 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a2623ff15..04e29eaa0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -369,20 +369,25 @@ class Dialog { } } +class OverlayKeyState { + final _overlayKey = GlobalKey(); + + /// use global overlay by default + OverlayState? get state => + _overlayKey.currentState ?? globalKey.currentState?.overlay; + + GlobalKey? get key => _overlayKey; +} + class OverlayDialogManager { - OverlayState? _overlayState; final Map _dialogs = {}; + var _overlayKeyState = OverlayKeyState(); int _tagCount = 0; OverlayEntry? _mobileActionsOverlayEntry; - /// By default OverlayDialogManager use global overlay - OverlayDialogManager() { - _overlayState = globalKey.currentState?.overlay; - } - - void setOverlayState(OverlayState? overlayState) { - _overlayState = overlayState; + void setOverlayState(OverlayKeyState overlayKeyState) { + _overlayKeyState = overlayKeyState; } void dismissAll() { @@ -406,7 +411,7 @@ class OverlayDialogManager { bool useAnimation = true, bool forceGlobal = false}) { final overlayState = - forceGlobal ? globalKey.currentState?.overlay : _overlayState; + forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state; if (overlayState == null) { return Future.error( @@ -510,7 +515,8 @@ class OverlayDialogManager { void showMobileActionsOverlay({FFI? ffi}) { if (_mobileActionsOverlayEntry != null) return; - if (_overlayState == null) return; + final overlayState = _overlayKeyState.state; + if (overlayState == null) return; // compute overlay position final screenW = MediaQuery.of(globalKey.currentContext!).size.width; @@ -536,7 +542,7 @@ class OverlayDialogManager { onHidePressed: () => hideMobileActionsOverlay(), ); }); - _overlayState!.insert(overlay); + overlayState.insert(overlay); _mobileActionsOverlayEntry = overlay; } @@ -1701,4 +1707,4 @@ Future updateSystemWindowTheme() async { : SystemWindowTheme.dark); } } -} \ No newline at end of file +} diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 3e248700f..32dced02a 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -372,25 +372,12 @@ class QualityMonitor extends StatelessWidget { : const SizedBox.shrink())); } -class PenetrableOverlayState { +class BlockableOverlayState extends OverlayKeyState { final _middleBlocked = false.obs; - final _overlayKey = GlobalKey(); VoidCallback? onMiddleBlockedClick; // to-do use listener RxBool get middleBlocked => _middleBlocked; - GlobalKey get overlayKey => _overlayKey; - OverlayState? get overlayState => _overlayKey.currentState; - - OverlayState? getOverlayStateOrGlobal() { - if (overlayState == null) { - if (globalKey.currentState == null || - globalKey.currentState!.overlay == null) return null; - return globalKey.currentState!.overlay; - } else { - return overlayState; - } - } void addMiddleBlockedListener(void Function(bool) cb) { _middleBlocked.listen(cb); @@ -403,13 +390,13 @@ class PenetrableOverlayState { } } -class PenetrableOverlay extends StatelessWidget { +class BlockableOverlay extends StatelessWidget { final Widget underlying; final List? upperLayer; - final PenetrableOverlayState state; + final BlockableOverlayState state; - PenetrableOverlay( + BlockableOverlay( {required this.underlying, required this.state, this.upperLayer}); @override @@ -433,6 +420,7 @@ class PenetrableOverlay extends StatelessWidget { initialEntries.addAll(upperLayer!); } - return Overlay(key: state.overlayKey, initialEntries: initialEntries); + /// set key + return Overlay(key: state.key, initialEntries: initialEntries); } } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b6a9e5fed..9955c2768 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -80,6 +80,7 @@ class _FileManagerPageState extends State Entry? _lastClickEntry; final _dropMaskVisible = false.obs; // TODO impl drop mask + final _overlayKeyState = OverlayKeyState(); ScrollController getBreadCrumbScrollController(bool isLocal) { return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; @@ -115,6 +116,7 @@ class _FileManagerPageState extends State // register location listener _locationNodeLocal.addListener(onLocalLocationFocusChanged); _locationNodeRemote.addListener(onRemoteLocationFocusChanged); + _ffi.dialogManager.setOverlayState(_overlayKeyState); } @override @@ -137,9 +139,8 @@ class _FileManagerPageState extends State @override Widget build(BuildContext context) { super.build(context); - return Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return Overlay(key: _overlayKeyState.key, initialEntries: [ + OverlayEntry(builder: (_) { return ChangeNotifierProvider.value( value: _ffi.fileModel, child: Consumer(builder: (context, model, child) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4bda68c2d..c444d1f5f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -62,7 +62,7 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - final overlayState = PenetrableOverlayState(); + final _blockableOverlayState = BlockableOverlayState(); final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); @@ -136,10 +136,11 @@ class _RemotePageState extends State // _isCustomCursorInited = true; // } - _ffi.chatModel.setPenetrableOverlayState(overlayState); + _ffi.dialogManager.setOverlayState(_blockableOverlayState); + _ffi.chatModel.setOverlayState(_blockableOverlayState); // make remote page penetrable automatically, effective for chat over remote - overlayState.onMiddleBlockedClick = () { - overlayState.setMiddleBlocked(false); + _blockableOverlayState.onMiddleBlockedClick = () { + _blockableOverlayState.setMiddleBlocked(false); }; } @@ -201,8 +202,11 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).backgroundColor, - body: PenetrableOverlay( - state: overlayState, + + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + body: BlockableOverlay( + state: _blockableOverlayState, underlying: Container( color: Colors.black, child: RawKeyFocusScope( diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index b61ce79a7..8320d08dd 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -34,7 +34,7 @@ class ChatModel with ChangeNotifier { bool isConnManager = false; RxBool isWindowFocus = true.obs; - PenetrableOverlayState? pOverlayState; + BlockableOverlayState? _blockableOverlayState; final ChatUser me = ChatUser( id: "", @@ -53,10 +53,10 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; - void setPenetrableOverlayState(PenetrableOverlayState state) { - pOverlayState = state; + void setOverlayState(BlockableOverlayState blockableOverlayState) { + _blockableOverlayState = blockableOverlayState; - pOverlayState!.addMiddleBlockedListener((v) { + _blockableOverlayState!.addMiddleBlockedListener((v) { if (!v) { isWindowFocus.value = false; if (isWindowFocus.value) { @@ -94,7 +94,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = pOverlayState?.getOverlayStateOrGlobal(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -129,16 +129,16 @@ class ChatModel with ChangeNotifier { showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; isWindowFocus.value = true; - pOverlayState?.setMiddleBlocked(true); + _blockableOverlayState?.setMiddleBlocked(true); - final overlayState = pOverlayState?.getOverlayStateOrGlobal(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { return Listener( onPointerDown: (_) { if (!isWindowFocus.value) { isWindowFocus.value = true; - pOverlayState?.setMiddleBlocked(true); + _blockableOverlayState?.setMiddleBlocked(true); } }, child: DraggableChatWindow( @@ -154,7 +154,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { - pOverlayState?.setMiddleBlocked(false); + _blockableOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; From ac1ae9fc3bbfb7c7cd343222f59618957637093c Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 10:11:53 +0900 Subject: [PATCH 421/734] workaround: PageView reload --- .../lib/desktop/widgets/tabbar_widget.dart | 33 +++++++++++++++---- flutter/lib/models/model.dart | 4 +-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 598b2cc4c..ddc51eddb 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -327,14 +327,32 @@ class DesktopTab extends StatelessWidget { )); } + List _tabWidgets = []; Widget _buildPageView() { return _buildBlock( child: Obx(() => PageView( controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), - children: state.value.tabs - .map((tab) => tab.page) - .toList(growable: false)))); + children: () { + /// to-do refactor, separate connection state and UI state for remote session. + /// [workaround] PageView children need an immutable list, after it has been passed into PageView + final tabLen = state.value.tabs.length; + if (tabLen == _tabWidgets.length) { + return _tabWidgets; + } else if (_tabWidgets.isNotEmpty && + tabLen == _tabWidgets.length + 1) { + /// On add. Use the previous list(pointer) to prevent item's state init twice. + /// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built. + _tabWidgets.add(state.value.tabs.last.page); + return _tabWidgets; + } else { + /// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal. + /// the Widgets in list must enable [AutomaticKeepAliveClientMixin] + final newList = state.value.tabs.map((v) => v.page).toList(); + _tabWidgets = newList; + return newList; + } + }()))); } /// Check whether to show ListView @@ -765,7 +783,8 @@ class _ListView extends StatelessWidget { tabBuilder: tabBuilder, tabMenuBuilder: tabMenuBuilder, maxLabelWidth: maxLabelWidth, - selectedTabBackgroundColor: selectedTabBackgroundColor ?? MyTheme.tabbar(context).selectedTabBackgroundColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? + MyTheme.tabbar(context).selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); @@ -1119,7 +1138,8 @@ class TabbarTheme extends ThemeExtension { dividerColor: dividerColor ?? this.dividerColor, hoverColor: hoverColor ?? this.hoverColor, closeHoverColor: closeHoverColor ?? this.closeHoverColor, - selectedTabBackgroundColor: selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, + selectedTabBackgroundColor: + selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, ); } @@ -1145,7 +1165,8 @@ class TabbarTheme extends ThemeExtension { dividerColor: Color.lerp(dividerColor, other.dividerColor, t), hoverColor: Color.lerp(hoverColor, other.hoverColor, t), closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t), - selectedTabBackgroundColor: Color.lerp(selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), + selectedTabBackgroundColor: Color.lerp( + selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), ); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1eac1be39..5e4693ccc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -17,7 +17,6 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/common/shared_state.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; import 'package:flutter_custom_cursor/cursor_manager.dart'; @@ -25,7 +24,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import '../common.dart'; -import '../common/shared_state.dart'; import '../utils/image.dart' as img; import '../mobile/widgets/dialog.dart'; import 'input_model.dart'; @@ -1348,13 +1346,13 @@ class FFI { canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } bind.sessionClose(id: id); - id = ''; imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); debugPrint('model $id closed'); + id = ''; } void setMethodCallHandler(FMethod callback) { From 552e45b320a6e1361580764332f8401801e7c160 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 22:05:11 +0900 Subject: [PATCH 422/734] BlockableOverlay blocked layer transparent color --- flutter/lib/common/widgets/overlay.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 32dced02a..ba7b8a059 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -411,9 +411,8 @@ class BlockableOverlay extends StatelessWidget { state.onMiddleBlockedClick?.call(); }, child: Container( - color: state.middleBlocked.value - ? Colors.red.withOpacity(0.3) - : null)))), + color: + state.middleBlocked.value ? Colors.transparent : null)))), ]; if (upperLayer != null) { From 38d26ec47b2d44e351661b71451afcc6ebfaa275 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 21:50:18 +0800 Subject: [PATCH 423/734] fix altgr Signed-off-by: fufesou --- src/keyboard.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 28e151580..105b84400 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -840,6 +840,13 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec Vec Date: Wed, 8 Feb 2023 22:06:18 +0800 Subject: [PATCH 424/734] fix CI --- src/flutter_ffi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ec4a90973..ad0d119d7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; use std::str::FromStr; -#[cfg(any(target_os = "linux", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] use std::thread; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; From 45c66060e5f6a0fe812ad2fb13b1b4889df82b3d Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 8 Feb 2023 22:20:48 +0530 Subject: [PATCH 425/734] 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 974fa86b8abb2fc90f43a069bc22ad71429d53c6 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 8 Feb 2023 22:47:41 +0100 Subject: [PATCH 426/734] Update de.rs --- src/lang/de.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 44bbafdac..1743505cc 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -392,7 +392,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "oder"), ("Continue with", "Fortfahren mit"), ("Elevate", "Erheben"), - ("Zoom cursor", "Cursor zoomen"), + ("Zoom cursor", "Cursor vergrößern"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), ("Accept sessions via both", "Sitzung mit Klick und Passwort bestätigen"), @@ -414,8 +414,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), - ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), - ("config_microphone", ""), + ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk die Berechtigung \"Input Monitoring\" erteilen."), + ("config_microphone", "Um aus der Ferne sprechen zu können, müssen Sie RustDesk die Berechtigung \"Audio aufzeichnen\" erteilen."), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), @@ -445,9 +445,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", "Bitrate"), ("FPS", "fps"), ("Auto", "Automatisch"), - ("Other Default Options", "Weitere Standardoptionen"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Other Default Options", "Weitere Standardeinstellungen"), + ("Voice call", "Sprachanruf"), + ("Text chat", "Text-Chat"), + ("Stop voice call", "Sprachanruf beenden"), ].iter().cloned().collect(); } From 244cfa25f14f9ee86ac8fc371f5ff1f0823c35eb Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 10:29:35 +0900 Subject: [PATCH 427/734] 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 edff4acbcbf1d932608765ad1ba1b5998517b10b Mon Sep 17 00:00:00 2001 From: sjpark Date: Thu, 9 Feb 2023 11:54:23 +0900 Subject: [PATCH 428/734] swap key update --- .../desktop/pages/desktop_setting_page.dart | 2 - .../lib/desktop/widgets/remote_menubar.dart | 3 + libs/hbb_common/src/config.rs | 4 ++ src/client.rs | 4 ++ src/flutter.rs | 2 - src/keyboard.rs | 23 ++++--- src/server/connection.rs | 60 ------------------- src/ui/header.tis | 1 + src/ui/index.tis | 1 - src/ui/remote.rs | 2 - src/ui_session_interface.rs | 4 +- 11 files changed, 27 insertions(+), 79 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 67eb0234b..4b6cf2a62 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -278,8 +278,6 @@ class _GeneralState extends State<_General> { _OptionCheckBox(context, 'Confirm before closing multiple tabs', 'enable-confirm-closing-tabs'), _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), - if (Platform.isMacOS) - _OptionCheckBox(context, 'Swap control-command key', 'allow-swap-key'), if (Platform.isLinux) Tooltip( message: translate('software_render_tip'), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6bb49000b..7db7d43aa 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1580,6 +1580,9 @@ class _RemoteMenubarState extends State { ), ); } + keyboardMenu.add(_createSwitchMenuEntry( + 'Swap Control-Command Key', 'allow_swap_key', EdgeInsets.zero, true)); + return keyboardMenu; } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1e4d80c9f..8b08e1e21 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -223,6 +223,8 @@ pub struct PeerConfig { pub lock_after_session_end: LockAfterSessionEnd, #[serde(flatten)] pub privacy_mode: PrivacyMode, + #[serde(flatten)] + pub allow_swap_key: AllowSwapKey, #[serde(default)] pub port_forwards: Vec<(i32, String, i32)>, #[serde(default)] @@ -1066,6 +1068,8 @@ serde_field_bool!( ); serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode); +serde_field_bool!(AllowSwapKey, "allow_swap_key", default_swap_key); + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LocalConfig { #[serde(default)] diff --git a/src/client.rs b/src/client.rs index 020bea1f0..fb255176b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1121,6 +1121,8 @@ impl LoginConfigHandler { option.block_input = BoolOption::No.into(); } else if name == "show-quality-monitor" { config.show_quality_monitor.v = !config.show_quality_monitor.v; + } else if name == "allow_swap_key" { + config.allow_swap_key.v = !config.allow_swap_key.v; } else { let is_set = self .options @@ -1274,6 +1276,8 @@ impl LoginConfigHandler { self.config.disable_clipboard.v } else if name == "show-quality-monitor" { self.config.show_quality_monitor.v + } else if name == "allow_swap_key" { + self.config.allow_swap_key.v } else { !self.get_option(name).is_empty() } diff --git a/src/flutter.rs b/src/flutter.rs index bee585d93..2d7d3fb86 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -428,10 +428,8 @@ pub fn session_add( let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); - let allow_swap_key = hbb_common::config::Config::get_option("allow-swap-key") == "Y"; let session: Session = Session { id: session_id.clone(), - allow_swap_key, ..Default::default() }; diff --git a/src/keyboard.rs b/src/keyboard.rs index 18314dbc7..00a2edd74 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -205,18 +205,16 @@ static mut IS_0X021D_DOWN: bool = false; pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { - let try_handle_keyboard = move |event: Event, key: Key, is_press: bool| -> Option { + let try_handle_keyboard = move |mut event: Event, key: Key, is_press: bool| -> Option { // fix #2211:CAPS LOCK don't work if key == Key::CapsLock || key == Key::NumLock { return Some(event); } - #[cfg(target_os = "macos")] - let mut event = event; - #[cfg(target_os = "macos")] { + { let mut allow_swap_key = false; #[cfg(not(any(feature = "flutter", feature = "cli")))] if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { - allow_swap_key = session.allow_swap_key; + allow_swap_key = session.get_toggle_option("allow_swap_key".to_string()); } #[cfg(feature = "flutter")] if let Some(session) = SESSIONS @@ -224,7 +222,7 @@ pub fn start_grab_loop() { .unwrap() .get(&*CUR_SESSION_ID.read().unwrap()) { - allow_swap_key = session.allow_swap_key; + allow_swap_key = session.get_toggle_option("allow_swap_key".to_string()); } if allow_swap_key { match event.event_type { @@ -237,7 +235,11 @@ pub fn start_grab_loop() { _ => key, }; event.event_type = EventType::KeyPress(key); - event.scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); + #[cfg(target_os = "windows")] + let scan_code = rdev::win_scancode_from_key(key).unwrap_or_default(); + #[cfg(target_os = "macos")] + let scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); + event.scan_code = scan_code; event.code = event.scan_code as _; } EventType::KeyRelease(key) => { @@ -249,7 +251,11 @@ pub fn start_grab_loop() { _ => key, }; event.event_type = EventType::KeyRelease(key); - event.scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); + #[cfg(target_os = "windows")] + let scan_code = rdev::win_scancode_from_key(key).unwrap_or_default(); + #[cfg(target_os = "macos")] + let scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); + event.scan_code = scan_code; event.code = event.scan_code as _; } _ => {} @@ -257,7 +263,6 @@ pub fn start_grab_loop() { }; }; - let mut _keyboard_mode = KeyboardMode::Map; let _scan_code = event.scan_code; let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 17d4e3768..9ce53c960 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -539,9 +539,6 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { - #[cfg(target_os = "macos")] - let allow_swap_key = hbb_common::config::Config::get_option("allow-swap-key") == "Y"; - let mut block_input_mode = false; #[cfg(target_os = "windows")] { @@ -554,66 +551,9 @@ impl Connection { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { Ok(v) => match v { MessageInput::Mouse((msg, id)) => { - #[cfg(target_os = "macos")] - let msg = { - let mut msg = msg; - if allow_swap_key { - msg.modifiers = msg.modifiers.iter().map(|ck| { - let ck = ck.enum_value_or_default(); - let ck = match ck { - ControlKey::Control => ControlKey::Meta, - ControlKey::Meta => ControlKey::Control, - ControlKey::RControl => ControlKey::Meta, - ControlKey::RWin => ControlKey::Control, - _ => ck, - }; - hbb_common::protobuf::EnumOrUnknown::new(ck) - }).collect(); - } - msg - }; - handle_mouse(&msg, id); } MessageInput::Key((mut msg, press)) => { - #[cfg(target_os = "macos")] - if allow_swap_key { - if let Some(key_event::Union::ControlKey(ck)) = msg.union { - let ck = ck.enum_value_or_default(); - let ck = match ck { - ControlKey::Control => ControlKey::Meta, - ControlKey::Meta => ControlKey::Control, - ControlKey::RControl => ControlKey::Meta, - ControlKey::RWin => ControlKey::Control, - _ => ck, - }; - msg.set_control_key(ck); - } - msg.modifiers = msg.modifiers.iter().map(|ck| { - let ck = ck.enum_value_or_default(); - let ck = match ck { - ControlKey::Control => ControlKey::Meta, - ControlKey::Meta => ControlKey::Control, - ControlKey::RControl => ControlKey::Meta, - ControlKey::RWin => ControlKey::Control, - _ => ck, - }; - hbb_common::protobuf::EnumOrUnknown::new(ck) - }).collect(); - - let code = msg.chr(); - if code != 0 { - let key = rdev::key_from_code(code); - let key = match key { - rdev::Key::ControlLeft => rdev::Key::MetaLeft, - rdev::Key::MetaLeft => rdev::Key::ControlLeft, - rdev::Key::ControlRight => rdev::Key::MetaLeft, - rdev::Key::MetaRight => rdev::Key::ControlLeft, - _ => key, - }; - msg.set_chr(rdev::macos_keycode_from_key(key).unwrap_or_default()); - } - } // todo: press and down have similar meanings. if press && msg.mode.unwrap() == KeyboardMode::Legacy { msg.down = true; diff --git a/src/ui/header.tis b/src/ui/header.tis index 009995f4f..414edab5a 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -156,6 +156,7 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Legacy mode')}
  • {svg_checkmark}{translate('Map mode')}
  • +
  • {svg_checkmark}{translate('Swap Control-Command Key')}
  • ; } diff --git a/src/ui/index.tis b/src/ui/index.tis index 20228ea03..ec2e0a748 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -214,7 +214,6 @@ class Enhancements: Reactor.Component { {has_hwcodec ?
  • {svg_checkmark}{translate("Hardware Codec")} (beta)
  • : ""}
  • {svg_checkmark}{translate("Adaptive Bitrate")} (beta)
  • {translate("Recording")}
  • - {is_osx ?
  • {svg_checkmark}{translate("Swap control-command key")}
  • : "" } ; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 2d0d4d2c0..999b409e0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -443,12 +443,10 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { - let allow_swap_key = hbb_common::config::Config::get_option("allow-swap-key") == "Y"; let session: Session = Session { id: id.clone(), password: password.clone(), args, - allow_swap_key, ..Default::default() }; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f20d1470e..96cd98364 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -36,7 +36,6 @@ pub struct Session { pub sender: Arc>>>, pub thread: Arc>>>, pub ui_handler: T, - pub allow_swap_key: bool, } impl Session { @@ -506,9 +505,8 @@ impl Session { shift: bool, command: bool, ) { - #[cfg(target_os = "macos")] let (ctrl, command) = - if self.allow_swap_key { + if self.get_toggle_option("allow_swap_key".to_string()) { (command, ctrl) } else { From 4f25b03a10a41adf3220a35944b99cfc6ff6ea61 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 16:54:26 +0800 Subject: [PATCH 429/734] 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 430/734] 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 f438dd9fd0b3250417f7ccad9d54768298232320 Mon Sep 17 00:00:00 2001 From: sjpark Date: Thu, 9 Feb 2023 20:28:36 +0900 Subject: [PATCH 431/734] swap_modifier_key() --- src/keyboard.rs | 110 +++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 00a2edd74..56e11f321 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -202,66 +202,70 @@ pub fn update_grab_get_key_name() { #[cfg(target_os = "windows")] static mut IS_0X021D_DOWN: bool = false; +fn swap_modifier_key(mut event: Event) -> Event { + + let mut allow_swap_key = false; + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + allow_swap_key = session.get_toggle_option("allow_swap_key".to_string()); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + allow_swap_key = session.get_toggle_option("allow_swap_key".to_string()); + } + if allow_swap_key { + match event.event_type { + EventType::KeyPress( key) => { + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + event.event_type = EventType::KeyPress(key); + #[cfg(target_os = "windows")] + let scan_code = rdev::win_scancode_from_key(key).unwrap_or_default(); + #[cfg(target_os = "macos")] + let scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); + event.scan_code = scan_code; + event.code = event.scan_code as _; + } + EventType::KeyRelease(key) => { + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + event.event_type = EventType::KeyRelease(key); + #[cfg(target_os = "windows")] + let scan_code = rdev::win_scancode_from_key(key).unwrap_or_default(); + #[cfg(target_os = "macos")] + let scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); + event.scan_code = scan_code; + event.code = event.scan_code as _; + } + _ => {} + }; + } + event +} + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { - let try_handle_keyboard = move |mut event: Event, key: Key, is_press: bool| -> Option { + let try_handle_keyboard = move |event: Event, key: Key, is_press: bool| -> Option { // fix #2211:CAPS LOCK don't work if key == Key::CapsLock || key == Key::NumLock { return Some(event); } - { - let mut allow_swap_key = false; - #[cfg(not(any(feature = "flutter", feature = "cli")))] - if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { - allow_swap_key = session.get_toggle_option("allow_swap_key".to_string()); - } - #[cfg(feature = "flutter")] - if let Some(session) = SESSIONS - .read() - .unwrap() - .get(&*CUR_SESSION_ID.read().unwrap()) - { - allow_swap_key = session.get_toggle_option("allow_swap_key".to_string()); - } - if allow_swap_key { - match event.event_type { - EventType::KeyPress( key) => { - let key = match key { - rdev::Key::ControlLeft => rdev::Key::MetaLeft, - rdev::Key::MetaLeft => rdev::Key::ControlLeft, - rdev::Key::ControlRight => rdev::Key::MetaLeft, - rdev::Key::MetaRight => rdev::Key::ControlLeft, - _ => key, - }; - event.event_type = EventType::KeyPress(key); - #[cfg(target_os = "windows")] - let scan_code = rdev::win_scancode_from_key(key).unwrap_or_default(); - #[cfg(target_os = "macos")] - let scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); - event.scan_code = scan_code; - event.code = event.scan_code as _; - } - EventType::KeyRelease(key) => { - let key = match key { - rdev::Key::ControlLeft => rdev::Key::MetaLeft, - rdev::Key::MetaLeft => rdev::Key::ControlLeft, - rdev::Key::ControlRight => rdev::Key::MetaLeft, - rdev::Key::MetaRight => rdev::Key::ControlLeft, - _ => key, - }; - event.event_type = EventType::KeyRelease(key); - #[cfg(target_os = "windows")] - let scan_code = rdev::win_scancode_from_key(key).unwrap_or_default(); - #[cfg(target_os = "macos")] - let scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); - event.scan_code = scan_code; - event.code = event.scan_code as _; - } - _ => {} - }; - }; - }; + let event = swap_modifier_key(event); let mut _keyboard_mode = KeyboardMode::Map; let _scan_code = event.scan_code; From c03adf53347ac519e2c4bb4327bbcca6599d06ab Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Thu, 9 Feb 2023 15:30:41 +0330 Subject: [PATCH 432/734] 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 433/734] 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 434/734] 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 435/734] 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 436/734] 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 437/734] 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 438/734] 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 439/734] 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 440/734] 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 441/734] 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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 -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 b29236da062bbba89ba3bdd6afeb70e7039100c0 Mon Sep 17 00:00:00 2001 From: sjpark Date: Sat, 11 Feb 2023 10:41:06 +0900 Subject: [PATCH 458/734] swap key renewal --- Cargo.toml | 5 ++ src/client.rs | 2 + src/keyboard.rs | 58 +------------------- src/ui_session_interface.rs | 103 ++++++++++++++++++++++++++++++++---- 4 files changed, 102 insertions(+), 66 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b315024e9..c171e84e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,3 +167,8 @@ panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip rpath = true + +[patch."https://github.com/fufesou/rdev"] +#rdev = { path = "../rdev" } +rdev = { git = "https://github.com/sj6219/rdev", branch = "sigma" } + diff --git a/src/client.rs b/src/client.rs index fb255176b..b98f9fde4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1702,6 +1702,7 @@ pub fn send_mouse( if check_scroll_on_mac(mask, x, y) { mouse_event.modifiers.push(ControlKey::Scroll.into()); } + interface.swap_modifier_mouse(&mut mouse_event); msg_out.set_mouse_event(mouse_event); interface.send(Data::Message(msg_out)); } @@ -1928,6 +1929,7 @@ pub trait Interface: Send + Clone + 'static + Sized { fn is_force_relay(&self) -> bool { self.get_login_config_handler().read().unwrap().force_relay } + fn swap_modifier_mouse(&self, msg : &mut hbb_common::protos::message::MouseEvent) {} } /// Data used by the client interface. diff --git a/src/keyboard.rs b/src/keyboard.rs index 56e11f321..105b84400 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -202,70 +202,14 @@ pub fn update_grab_get_key_name() { #[cfg(target_os = "windows")] static mut IS_0X021D_DOWN: bool = false; -fn swap_modifier_key(mut event: Event) -> Event { - - let mut allow_swap_key = false; - #[cfg(not(any(feature = "flutter", feature = "cli")))] - if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { - allow_swap_key = session.get_toggle_option("allow_swap_key".to_string()); - } - #[cfg(feature = "flutter")] - if let Some(session) = SESSIONS - .read() - .unwrap() - .get(&*CUR_SESSION_ID.read().unwrap()) - { - allow_swap_key = session.get_toggle_option("allow_swap_key".to_string()); - } - if allow_swap_key { - match event.event_type { - EventType::KeyPress( key) => { - let key = match key { - rdev::Key::ControlLeft => rdev::Key::MetaLeft, - rdev::Key::MetaLeft => rdev::Key::ControlLeft, - rdev::Key::ControlRight => rdev::Key::MetaLeft, - rdev::Key::MetaRight => rdev::Key::ControlLeft, - _ => key, - }; - event.event_type = EventType::KeyPress(key); - #[cfg(target_os = "windows")] - let scan_code = rdev::win_scancode_from_key(key).unwrap_or_default(); - #[cfg(target_os = "macos")] - let scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); - event.scan_code = scan_code; - event.code = event.scan_code as _; - } - EventType::KeyRelease(key) => { - let key = match key { - rdev::Key::ControlLeft => rdev::Key::MetaLeft, - rdev::Key::MetaLeft => rdev::Key::ControlLeft, - rdev::Key::ControlRight => rdev::Key::MetaLeft, - rdev::Key::MetaRight => rdev::Key::ControlLeft, - _ => key, - }; - event.event_type = EventType::KeyRelease(key); - #[cfg(target_os = "windows")] - let scan_code = rdev::win_scancode_from_key(key).unwrap_or_default(); - #[cfg(target_os = "macos")] - let scan_code = rdev::macos_keycode_from_key(key).unwrap_or_default(); - event.scan_code = scan_code; - event.code = event.scan_code as _; - } - _ => {} - }; - } - event -} - pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { - let try_handle_keyboard = move |event: Event, key: Key, is_press: bool| -> Option { + let try_handle_keyboard = move |event: Event, key: Key, is_press: bool| -> Option { // fix #2211:CAPS LOCK don't work if key == Key::CapsLock || key == Key::NumLock { return Some(event); } - let event = swap_modifier_key(event); let mut _keyboard_mode = KeyboardMode::Map; let _scan_code = event.scan_code; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 96cd98364..d55073b9d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -335,10 +335,87 @@ impl Session { return "".to_owned(); } + pub fn swab_modifier_key(&self, msg: &mut KeyEvent) { + + let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string()); + if allow_swap_key { + if let Some(key_event::Union::ControlKey(ck)) = msg.union { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + msg.set_control_key(ck); + } + msg.modifiers = msg.modifiers.iter().map(|ck| { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + hbb_common::protobuf::EnumOrUnknown::new(ck) + }).collect(); + + + let code = msg.chr(); + if code != 0 { + let mut peer = self.peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + + let key = match peer.as_str() { + "windows" => { + let key = rdev::win_key_from_code(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::win_keycode_from_key(key).unwrap_or_default() + } + "macos" => { + let key = rdev::macos_key_from_code(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::macos_keycode_from_key(key).unwrap_or_default() + } + _ => { + let key = rdev::linux_key_from_code(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::linux_keycode_from_key(key).unwrap_or_default() + } + }; + msg.set_chr(key); + } + } + + } + pub fn send_key_event(&self, evt: &KeyEvent) { // mode: legacy(0), map(1), translate(2), auto(3) + + let mut msg = evt.clone(); + self.swab_modifier_key(&mut msg); let mut msg_out = Message::new(); - msg_out.set_key_event(evt.clone()); + msg_out.set_key_event(msg); self.send(Data::Message(msg_out)); } @@ -505,14 +582,6 @@ impl Session { shift: bool, command: bool, ) { - let (ctrl, command) = - if self.get_toggle_option("allow_swap_key".to_string()) { - (command, ctrl) - } - else { - (ctrl, command) - }; - #[allow(unused_mut)] let mut command = command; #[cfg(windows)] @@ -851,6 +920,22 @@ impl Interface for Session { handle_test_delay(t, peer).await; } } + fn swap_modifier_mouse(&self, msg : &mut hbb_common::protos::message::MouseEvent) { + let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string()); + if allow_swap_key { + msg.modifiers = msg.modifiers.iter().map(|ck| { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + hbb_common::protobuf::EnumOrUnknown::new(ck) + }).collect(); + }; + } } impl Session { From 491932cda104b517ef236b27e026a603831f1400 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 09:57:27 +0800 Subject: [PATCH 459/734] 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 460/734] 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 461/734] 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 462/734] 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 7fb78ebc743d0f3449cc2a397b3b80b26a55410f Mon Sep 17 00:00:00 2001 From: sjpark Date: Sun, 12 Feb 2023 06:24:04 +0900 Subject: [PATCH 463/734] bug fix --- src/client.rs | 2 +- src/ui_session_interface.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index b98f9fde4..78feceb7f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1929,7 +1929,7 @@ pub trait Interface: Send + Clone + 'static + Sized { fn is_force_relay(&self) -> bool { self.get_login_config_handler().read().unwrap().force_relay } - fn swap_modifier_mouse(&self, msg : &mut hbb_common::protos::message::MouseEvent) {} + fn swap_modifier_mouse(&self, _msg : &mut hbb_common::protos::message::MouseEvent) {} } /// Data used by the client interface. diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index d55073b9d..73f16478a 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -370,7 +370,7 @@ impl Session { let key = match peer.as_str() { "windows" => { - let key = rdev::win_key_from_code(code); + let key = rdev::win_key_from_scancode(code); let key = match key { rdev::Key::ControlLeft => rdev::Key::MetaLeft, rdev::Key::MetaLeft => rdev::Key::ControlLeft, @@ -378,7 +378,7 @@ impl Session { rdev::Key::MetaRight => rdev::Key::ControlLeft, _ => key, }; - rdev::win_keycode_from_key(key).unwrap_or_default() + rdev::win_scancode_from_key(key).unwrap_or_default() } "macos" => { let key = rdev::macos_key_from_code(code); From 01d30bce9e4509b6129843bf2d460d0351c28638 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 12 Feb 2023 01:52:11 +0800 Subject: [PATCH 464/734] 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 465/734] 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 466/734] 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 467/734] 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 468/734] 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 469/734] 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 470/734] 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 471/734] 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 472/734] 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 473/734] 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 474/734] 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 475/734] 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 476/734] 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 477/734] 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 478/734] 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 zcmbUIc|6qL_W+LH=kqzc83tqD#!d@kO(IWAr6}5j$Y?=IO%&RA4wCjMD!me;R4Ou+ zN{FXON=2!(VX}+tAqyYdJ zD?L5d0YH!y34nsgemtvxJOBXh_4VGcOjebXlY=10#KeR`p~%WADk`$7l9Cb(!?FT` zAhKLmrc$Y7GWq{kmH}nRGN3FGi9}gWAP{7&GPo?ssw5JL!C=UMvh+W~1cD4G8(h{Z z{r@K!d}d~5e0==ZuV1o!Y;0^`U_jRB=jSJb_x1J3@>j23efaP}2ArFl`|;z4wY7C` zZ*Ngi(a6Y%4D$5p)1IE56DLk2CnwL&&X$*#PfblpB$B44CRwYqvvXEf7KULlF)_00 zw{PEML&>^ux!j3~iH?qr?(S|Gf(-ch@#E3a(Z^S&u7~~9stRay52Z(w=#wfVg2X6I)f<6!{ z0S|gWSqFI71Ni^I(@s#-3r-S43p3HRkV`^Gr;?WpjbRPX}V9F7-s4A$wr#(yF!^x1&Sw7EEN7GE*hOMM) zp=7*3%~UAH)jIpm*BKOIA>$vn<{cPD%Qh&U{&_y&MV!mWOeCyp6rr zkL*0*aeV9goo^HL0XX|_rHAW=`0oQ%d>KuE3Fq#p-GTpC{Qs7?69Z<%rNeE0T$=llOn5)(@!&-=Z5I=!kSz~U)= zUsIRe_mZ7vi@JD^rdN&aSaWaR;=KFVlIo?m?8HYYAsXc(ORL(OTikm44;7Mb;cs;Ia&?`Q;4ko9Y{OY--6tO@~;ri9& z;h3^qAL5c`jk(zntuSfQ%;P*MJo);J{8z)ip0zK6M3%4L<&aNSZRNVu22LL=j<6Id zJi6!7pjbd`4Gy0g^LSt$AM(03$Mn?m%lOr4?vQ-6Rtnz?3mgr0+q72I)79+_iMS(O znI}D59K50MuHd7p(r|g)agBkY2RjSq4yIgF6}i31u?j>-A}`N8bC)ywZ?wlW5Ph&f za>Y%~?80dHyoExC!(9zFIzL&5u20zFmKuzC%k~Z&@3}XC+5~Jl*saZCMm(t*3$Xyehpy_4jqb z-)z)nhIr1O+a|Z2f68IA}K6tIs8LF(b3NLW}D~fSyP_Vn_TMgCeUF zcb{W(O^!qMp5)A9Q|Hb&Zc)CKnC8VYlbBlTh(jjY6Q8Um`^wN)%)0Ly& zD%BU=Sd`=>_2FJTkI%eESOxV|GL~}SBUJnNch1MZj(Fw(z((z;WRTJo2f?U;GJyt8 zynQazdgy{NkGYVjfb!Nu92Wpkwq<}k8?Y_L(2%ZjE%t?4P4W8sWXjD*J51pl3FB?~ zwj{aPB*{#^tRP_Gi~Er7MuIa{F=dN~;>pESKUICQ{`k6`rxvE_uCnAee#B|@x)XB*h`cCC4lwH+Ir$R5=T<(W;eT4ItGVADuaK=xE#!@95nAUHuDR zuKCfo|9e81i($F3?Gssg>T`DD_Yy6wy-WQ)i^WD@6J~I$d~KU^kjvae>?OhA={fJ3 zAIqa=dVAj29+o?irr4px+mC30E>``L&Yy`t690{M71-2!D&Kub5|-VHcl&XMgp|1#{M9=4y6*=!JhCddPC?AMZ{$>l9nt7V> z+w|wo6Rsic^1Q^1IC&q1hpWp;yd+IrJ_7p)Z!3&VQtdsZ_(v&-1JPGMczC!}nkCqg zk`L`3-B|?fHYYBKH)GLug|GOkn9{BK(*!Tsp|5ZDMWNC;e2OmSAu09Hth||>wEXv0 z@Z#-T?`>LMd?xxHI>3;_@QnWcnT8*)tw~o9mgHJu7@XkNqvLIpq>ffC1q!go4X_7TKF;~d@QhxKd?oby15ek)>B-v~ z?V8w)?;vZ>O73xoZh8Nj!t!a1DuV+Xw}( zY$b+)1)#Jlac1%TQcdnK2_=tjeVg((h+_iWdixp_O`g-8$fCYYk%sw~q0!E!>->XQ zS{^A<1IPi$_HVu7Aq_lg6Y9qWwX$__Ik^1KlJ`W%=*`KW2wbS~%A28NZxcffc5f+_ z9*(W!BArqV@!$UDA6lxTdd;g*AL8%-Qj6e!=h|CcQ!mtas>}BL%zF^qycWIVQK7#J z?7?1sj@nI>7!t#^Je%#xDQ{OiczEU4ekN8=XGR;QIjf5E`(cN5E7{5F++VO|9gwgy@VRX!u3K>|qUjBS(%B zgnT`D0iUI-p-un8D2aqM2)(KHTlH7__xPMIgo1M6hvW0JiDs){FCCVzqsz22L-c|r z2nnjO7GCQNR$fjO?mD5R`Y~@be9s<>ChA_L%w0LelH?zhhskZsb%FudPqkrjbuN0! zO5GUSlj(ps_0)H3^R>{G@`8)M;jkND$zorG_5Bjxtil3&aPk_Oo<|L33j6;%VS~Eu z8;U>64#$F4ASG|&;CrjWAmTGk?9rj5Ye1ivOK!A+^D_EI)TnIJV5H>ee(3DDpGd%K zLbo8iOICDtI!?e=3eVc0|IPTUUj6Q1-S`_Q8b&;>A%uCb>a-PZy@wKbL&n*ui|Nkr zZr#K^7-uoIH5*kBjM)h71id{K7m5z6NFjcs&vLTeC311Utmn}w% zH&=zWYDF9$gu~2;-z-2myj;o32cj>8;;6!h&uo|VRg!q_>iEt@*jNKN+YCMfJG5`M zzE$hJgggj4)HkXKwC%UGAhOM(AE08|(wmpSCDwju@EP>ef>bBpnSiYuHsTDmQ9DW4 zeQZOyDc|v{yzt@T3XdyW&FZ0`Z<_4+)|ep^2jNf6jZC3FED-Dx}`a#7Jou2##D9Hy|M)N)3rJh5k zwdrQ7HG?3)Yr;3|-iEd`zFjM)WHd8&okQj!#{7UQ;fN)^I@f?W;E$=Fh@zsGL3SX| zEK&P*8bg$Ki^QwmLyRFh=sZoj*Qwm4;t#_3uV8srQ z8;RS=OBoqO(5M?S<&ZSymiwV(0;KMg=O0um89k7YkhQNW8YJ-y(-Z^L*e;9|O`_A; zT?*XHYM%w_87>X$Jzz={mUS8mx?|MXw;em{#s4hc%b0(sL=S@>D3oY4>*Ac_x|`YY zaZYV+A3@A{XRS9TLeTg-M}H7ba@NS$ zCS?sEDG4E%&g-(|F{MX9m8u)S^klqxNahsR?3gcMCHi>3t=AIf%s`Ib$iJLyE!VT1 z4E;y6Xz)ad-9jvw-Gl&Z+~4Uiutq!s9`w8#At+)QkeM2X+Bv0AF}^@(4$;ygd!d?alh^52{uiwRs}_t7^IHcFiTHvSJNV+2(7I zs+Xe83%q!8i>Zf%&3BKb*~jYU7xL_B1~duuez$hG&9}=(uUyL?`!bZ!X&E2VdSvKu zn{tTMlQgV4M@xG9++TovRgT~ML(c39oma5xJw3A=Q1l4Q5NSpJAw4dr&&IPC z>VOGCV5QFP3a$KK=Kr2!V=9pbAQ2kaDF`N%On#}nzOk}u0siP7;-NuktTjD$OSl@) z)!FuSv^BZ=)3KON4-kqNT_q0xYdUGUt1Sx}n9o1|H~@FMh$lQz^6yp^^NudEQp8q3 z8`8p{pDlsEMDKxUzB(7HOVVQ(SLp0cT*TXuq$);}|D)g`z#|I1+OUbhCHeXb^@QPv zEx~)*7R%@*@mwc{_UdAA+F{M62px)|J5E}u)wpTx3>9Mift;35^*?kjV7I%3Jxv}- zz;Fk1+cT24DaaG8O=Irk)~I|6b#m4KP8AV)O32I*UH zuKro!(m?W5ZA3@VwM$x=yD*g`O>uS4eDkTwtDvn@MYu`Y7EL@JxXx^z9$sO}%kV{A z9&0-&GVloZd@O~6*6_dd$*cf=wqG~GXJpF8Z0vA*2JSF+$Uzb-f=3qPcOrKz$+f~n zkX`;iuxw`U8OZ6aZKE|(9Nr6ld6WD@>b9VIkJQ+YLHjRs#ieN?#S6Fz#-i}6 z5KEF|=$SAP{w&Gs2D~ML`G%;xscODgMkvL5!Kr*=V71ogud9 zwEg?f<^lY8UEZ;E22N|Jg8hG zJ*XipQ#TeZl2i_FLL-hr?RSKY&?>T-9+VYmP|PfGkHGe)g3XH(Lj#coowN!1EO5>< zq%?r;CL9-%)g%4^fu1;)c9V@6zH`)3ie?l9{&t%$hjLuD=x(|6 z`CK!^LNdb%-!{QNo;jlt4a({b{tQj=e(;%ay3UgD=!RD@MObUb?RbbYxj^dk4!*T3 zq)mQ#7hZn-VmEou(G=F0&;?Ghi(tYyqZEb!_-zKaU zX=Ne%J8v8Na8owI?pF^(BU4gaVvRYej!x6ru@1jxdfGvLs3=_@+=u+m+4St2EX+Up z5_BvF_oZ7G^RpWv=ju(Uk13eRMvs~^?-6J<+@+YVQl=HdyBLg~U=MtfywP1n;AK2J zd$?g)*VS?GkT?~-hqU8jW#}4lsS``kgw#}Ou$Ma~O+T6ioyWldTDZzs&9@V>(Aj== z+t`i#!%oPQnGQH$mA5S!x}G=p$_J@Vx-SN*@Ya#bCRo*O9d4ew&}9#_+_*#`_P>*A zg1zv-eOm>arz5UkDr;6J2cT-(D5EBUpdrD{(>6zsy7!fNAyUMZAA3h|wQj z3D=bYhqH@LoQ7&Fcn7{kX<>I0Ujm+j7@-RzfB`6qSc+P{1G6g*6U{v0Nt7NgsM#Yt z1Twpj(^Fd5A(MRkgUNZWOk?L<9$ELP?Cue&6|sqMLXai zv7mk}a3lK)7NWZw5gyDQf|VRL-y1S<2VU|s*r4;Sodj$>e6%K(V?Y0^*HcIt{FXL^ z6LvoM6;1b@-GW_!S5%q_=`^tI_&;qyl16+vt@FM@Z|j&Jyc)_VIdPOYm8OeJwxG4I zg_JPNUiP#u+)jM;eF11eoEBD5u#QjB$pk3PK{%ZdeiVHu;hTV$zy^MRwFlD z9_tidk6S^P*}<1K3la-zK$yVu-{I8`}!{~IM`~}m;#qBr#Dvp2F`TkYYyrD z?OS|T$sTcy6Q%&!+~OxoK5ao4>w?od-vW&H896PM^N;yNDf#!{Vv^sq?EBiEgN6;% z4OjWS$@=Of%A*D%VQLfLaR~kjm(B`b)%YZtW!(xTFxTVD->o*}mP*dA61O+Kc(95c zYl&AFMx2jB^e(#tro+l=il6{qxMU|`mff!>#6qW4fF*f)_Eiex)UlIWbeu<2`vMi4 zaCN>j=u-;ak^b}vKr_CZ7@Kf{01faifu+T_&fk~(+IkW7sV571)uTv#MOZ}ZPHw&x z8gEw|yNKxrle-ENV?$j|_~S;3`+J~nZI8L4!pBBj4X%SUme>5d1)W{Mp64W2@}FBt z?_p?%L=rv_QTX6E?#A1SeC=-y5T5a~zqJwnmGcqG&{0pc^S&OjhU;)qNws?~ne4BF zt|5Gi=Dh&_*D?I1;Pqi2eJ_ z*{#6TABB8V*#I)omEHfBCT4OTRA&K-13yI}Lb|eB^@-o6X1^U(vTMm51F0rO*q-c_2Xr3#h60u0OBJYgYp-4_>Atb5Pob9|+?=N~wRBI^{$^mgJ)I_Kjn_L2`D z8-sv5;91~5mGeK(86kvQ37D&Se~!-A9|dK53F^w2S3UMnM(JxGbYys|XLI*E;hI}o zfJeCU7Jl(BxT^;KeKTqIHVvf z0oRx?a8kd{?b+&C6dp{iF-rpwfTV~F^D$H3Ux{-zv88_B{#1}eUIlQ;Lq_>x3n`0O&^3>rL=fEP zh9p>wM}9TF+Jwhe$TPd2!$~8R;J3s^L!3DGDE)Emo}OkXjskz}r$DJ$Mhg?kC0` zdGhTi+g0%SC?+u**tm5H3c6Vbjb}OMeV%OME@CEESN?j;;+tn!joAY8VTqS;O&xHDHTL~o9{lB*Z5*l@_t&~Im$Y^@H|!v;(fT3#Yt}uVbZ@vybOHlumkHzTcr=s znp`&0)~vx@J&#}V90XHDR-uZ(9*4{!`mZ`9Dd+`<9}r ziB=v>XNAfm8&Qj`y4<{Zl8$JgCiP%}i8j*pDBQlu1}p3HJx}NjJ~oei?-8vxnJO%y z;mY_+7sZ8;%=5Efug>q$WjN8(^7n>7BX9uz-KHQO8|vM|^{et=)=~$m-y-5kq_ces zSuQSlF%mTUD$=JK;Jr8TMNQtGdTikiRJed#7uLTYw!9k$e{N&cG$7+;Q|DMj{C(6D z7jj-dc%sF?Hn$j!%h91c2EKj6BIaLv-)w+5Q3dzl=&$q8x6pJ;kBbov^v5Nw1?txz zy36muH8GGmFh-f8d1$f%b|3yUL>7ly$`h-{K*_yO3u#fIv(A!0O?`x;0g}&^p12Nt z^IgdKj+&GqD$BSB3Cg$&Px}(o1uaUoZKUpYlcb>8=1F09MHnX!uZ=vh;SgRQEMqm`$mls|)_#D%<= z^ePQt1@vK#%KQ2KM4tIiX7oN{8UHl=doStlH;9#CUz*kLUJ5bijh3rb$Yl}do92%$=N zEq{9{e;CC&eMLzJLFh&^7(^4)p)I6mQ)-XPnk0|$cPy~^Gy$L4vv%h#7{ywXc`V`?O-Bn)NzD$NSK@XAmU&@B@V{O!UDO|6k>AO->dv)y{0#MM+F7eXj_--4e)ZHA4=;5)guse@2D z?XoWT#KaTTdK-*qF5+w8Ge#S|<*|Ek()5#K)s(us2|5d0f$KxdVnkU4!|R~jhMC!#W?U>pzH+BSW`x!2gW|? z>yvNX?DOe*I^54q1B+QukVLQOe#)E)SNSzKJs(O<_l~y59F!^MrCPm3t(Qb6j<*GL zdhHsoe_QnQ??143`jOuv!OohOE(N3!5J7F+sR~GX#t83poa=}SV!BJKyVFSik+*_$}H@U z?wxHLYIuI}Q}1}u;^82W4Gaw}Bpn{k*po}|z98JI0qIyCVn#DTq5be387o-!6T)&Y zyAIzMkG{L`RCC_yb2nnJAY0jDob%oEVtPhQ3IlsD9Mpz92>2M_dn{tkS%5ymdzV@3 z-ok8B$mw@927kH-`q`5h^~V@|jYuS+KvZwDJQM;zisXF-Q=)01PihT_sUsnjdsLpuK2?dwR#m%`b`* zCFc@mM+TODe$-&O+*$c^=us+K3bda78L%aTy(xG@GC`TJGRer#yANW5P;|2m_b%WbdRGy8sHb_k;SB|d_ZZ|NC4+~sy$!B7DVX!1(Y6 zp9{1cgax$Fxz8Mw1HGyFYqR|3gKwU2=iM0aCr9ct7{YB|4Cc=4qlhZy&=q_c$yQmM z%BS-(AKDi3HXxqS1{Wsb?!B6| z=`gTxi8qAL=o;8UAI*U8#&H;1=o;OEVqk~Oy2Q2a+a5J0U^i6Oh)sR2R9hvhW-_=-nHg(nRD1rh`2lU(P?&zA7D`0_2pXRRsYoHnr zLGUjhJ4c)MRL&yF$}tzL*2NH8xQ6`>*l%tl83ki=*|A4g-@LzEiLIAVMexl*roPta zYC{vpq)E%Y(wl6W(k=I&n>#*|bHWHce}qb*gePamPhS)Q(Gnzr28n(&6;2vSjHAK* z;_KS@Wq-x}4IhUWc`lHcL%wUcBdL%*Hm3`oF-i|!g@AhYs$(yd(IDkZ_m`vmBd+}~ zkQxV>oEvL%zTI`A*5xt)`Sht@J1@@#V=|#(vK{!W_pW~pZPgF;^K1e2J)U~GkdlX2e3-bgz*Wi;eI%|WkhzJs8RHSL@DcXy@gT%{SjJ(4M|xV^cU zAkqRx2&W2}`menmFqyt9k(xhZx2aPp%4sn6t~Dp1Jx98hVipZpM7Vs|cK(ORnRShJ za={pqy=E=h*Bi>b>*kUZGV4wUm=QQ=Rdeo#QuoQkjqM{jcL?adl`6iSMaidT`Jz}o zUiw@SR81G&sBfCr4Xo*D1hgCU?H~BenWr)aSdc>SNReB6PvWgh3cj|x)Uhyd3Bk65 zs6W>ZLA)i#vY<6D=*8ZxmS{2TbMtehoWrZGz{GhT9n#URZAkzYv9JdC=ZA7_wqgaT zSW`Bwh^8!vUg}A*7yY}ps`-A-%@;~+3Lty=F_t!9Z5AlAyK$e3nmEPr2)`?o_NXM&Xus8~AoDCga6*^5%`}kmrAX-%pEl12k?K(a!FTeJc#tBC z;l#oSUv~iyxKV@cE&T`nCIWT#vhSE1gR?9P-J}F4!nM1IU?IL{1!1^2*$N$X*@4Z3BdaqKOFu>*wwGbAmHMsW>V1~QdSD9m=vX>eHf;Ua zv}pvESI$tcK8^#H9vZ+8oo!IaPgK6S6bE~e@m$}j5`ip$=W(JI^8#ieTR-eN_!0oP z2lUvVTo%2Z4$mRIu_PXfQQ#IiK~AZ^i{1n3dB7uKYp@Fx+TRAbeCcNmVMrZlkApA} zsY`6_u0!HK1Z>AFa!A5BNV%HKQI`#+w_$D@OAzhAAHB^#5&>B&`ySF!CtxH4sML#u zt~&*r7X< zj-qr`Zmg$!q~=${8IUVk4E;?UX1@R_-rUD=NaczP!|iO(YFE~ji5qxA=h}k~YX--B zJ9c|5&=OB3*1ZJrYJdSyTY4uE+8srij|D+OGnA)UQm)4NKp!@uagkXGa#Dl6p@h)0>9i+KtMt%0I~^IulZL=x@jiU|vOb2>eMLw?l$tR@iO0^WSW^ z?l7pOH+~^|4vc|Q$K4K_AV1}2q`Q6p7HM#0UdxL%d`G2XF>>Tor?_Dm`-3@tE$h2< zt|j=6oVwaBY)jt)*&bP5`W-R@N+@+pRGrvEMDLf?sIC$8G-_8`8=?HumV&u>KB;t~ z2|W$&hQZbSiKk0Ow|q`W4LzZpd26u&7afLfX%J^eAYwJT)Ckh27?sKKa$86w(Qo)d zb;chqviNe+2b<8i8^FG|EwP5oE+8>3m10m8m6b(ERmDk*eo&D8e(#Xe3zc0R71d#I zx`bRQRZ(Xz9EaYVJ(jFuwvPfN7Y$a8t!a zlE0ZW%R-owPPuT6kVIzQhc{Js#FnK%rO)X2xXf_K)l?^8ryKwJGy_%3)J`iblhNxV z$BuxvL_Y1X$G?y{FTsyf`k??%3x9R?%Eh8AbU)LxQYqC2%V+eyS|&{Gv}gNcrjEpXi)Bq8#nx^VWhC@)ZrV-beM$;eiZ zoi_o54KN%?_Uf4gWp#-M_xgH zM~iKnfSWU>upWGi!e5O|pF3EFr8qGNolztrf~kZD7Jhj^oLxU%z7%zNDkpdV9UTVt z9yz3$p9Gc_db|!$d+KxbAv~?8(3phZ=1(XA-y`}SVdnligm3v)$nNwbYT`pSXVui% zlCk5S;EEIYpQOls3^GX$F1Dk9Xe(N!i_W8o>JcK_fmHr&Rxba@OL)097CYZT{~zeM zY3iX!qENx|@|_Tm!EGh`hMXQE!6}EZ)tT_8X#+uTKOS-w`#KJ_+cU8P2A)m)mZR7Y z*|9{ycJ*93$Mz^#pnc@0Ocuh^Cdh0WLHybR%Y;k5ZeYtJs{JlSa-QO%hYCzh%&y&4 z4m$u%Ji_*mpcM2PS-o=?$Th>eD+-9Rxlk(T1RX3DHAss!<#+>)echg3q1!q5!4uxX zFihb*!G4$2akw@$h}pYqy}OwL$D#>5=Ml1$_4vTS4zoi%J)k3X5QW4aL_g;&w*wt# zgoMLZ9{RRm*NZX3N)2v`75;D4qND|O=o1z8M`ZUQF=U%Zk@V3D@9$8-<#&naFE0H|9T>ftR88P6jE^MfiElvX7E>s#VZ^|Wv}eb$1lZ-qL*_~}I}Sn=VIOj^mX6xMRh70@`apC(FlcFH{f z6gGdZF^gl!rb-V#!KFEPf-&vv&>G9}Y`3z&`AysRwZIMn?ti zee$VsaCOG14yr2#Ay+BI3QT1s*GogBZe5z3Jlm2>^S=uTrpYAr(J6ZjOY zTaTT3=Tvj#3Z6G=%W=SdI@~;&6NzMnmnq16B+C75WB}(MWcf#`i7OTp3d+?Q^><<^ zvs}*-_{BNpgLF=k&ptIy=PIJ*7+yLFZD5sj*P}m-Yq5AT+D5l-2WcdsHOd43Mq0Um zcuULm&I4`#TafOu{$r-=v5PaWN1qT3_4oSLD4ACz-v!~%QPm(s*LvInf7?T6llD^9A9OCCun_0EH=gs!0vTh#y)f#R zc#p4EE6H}fT*8mZV{}395LsPL9rMGuLDYU5e1rQYQ606s3;S?Q86WZ63;8c}SW2Dx z)D|v#q4fTtXl{zc>NB9Eb}wV327AJLKq~po4t=R9+IF2a{vc-`DJ^;i^5*du0N<)4o4H>r zN&ayVDYP0>&BWd!GE=x=TZlEf;T>qN#;PvKrViwukn=O)t#^VkjPNm159g{ko>UZb z$$ErCgE4h~V25~hwxY9Ut#6U>X#!AP@+*X`?0QEV5v%cI_rsdLz$PcRn*~bg1`zi0 zo(O|Af75C$;WE=g3Y^uN=L$L8A_`9jP_NpdE0IZ@_8f=S9_?D(RY6J-j*?z2~{ll)p z!;*OhkImU1Cbr6CqfsWIu^plRElzoU0OM}PwiRwcbYEP>e!}@Zh|y-Fq#a)GP6XKDnsgDuy{TuP=#mthWx8hO_s-ZgNk6Ns*1 z=bTel>9Oz5lUuW&gBu>_64Ei(#R?+HZsCjiT(wJdFrQSsoq*57hO#%IP-d!NOogG} zrjw*EGQ+Mb=eII1s4n5-YA;G6w`nZvSMMF__kg$QDbMS{1`XYh02T`%+vtSOz z9!b%(1*Z_NR+8+&w}Dlc8ps=`4F72Z!i|X0zbLWLK-p6nMdJ0+Y|(;Nm7urK&W$F_ z9$5FWI8^WqcHkR=LCC@mOxydu1{^xdS0FXR$Y5lC2~{?(TPZ$Vlo?o!zEL&4t! zY>Y@Bey6XFv>_Y1p{*1o?^!=LLoPxlZerBGQtO=YKiBY>+fP;!J`lLNIq)@2ZvK9) zx@F)kap1cc0;|zM=>89YofXQa*GS-Mr?bhcgR8o3yI$W2>WIb0H2OH$a_E8*_n^+{ zgH|afpowdPy=7s$sYn@SZwu@EMxC)MulTQ#Iv>au%B<}Xu0q5ijF}!7dx+T+`1mfo zxizsGWWd&^$xtekdcaObyf~EL&%~($nEMo`(V6Eo$n|U@nVvYiW zk)?mj&z~d#4jyxytA3bogouw~WO+CU(vEmQ;1aOS$d;kzbdX@QmuNDO9u7(g_GgPA zV!MgM6FDdFz_3;J~L~;Aq11XtU6 z{iysAf+KunXmZAos3&Btsz+s53KbOFN;%JFDo`dp>DtNBhRO#Ue)a8;Jxu4tbEGs| zeEo5*_F)KvQg7bFY_BD1X@X*s$dpPkz*fu=x1YfV+H#B`kMCLNNtwy;8s=`ISknM+ ztzn6|jZukA${jE`F{#1bkPI0thqSM((&Y;F%H1~R+GGw)MDD~gpAvIgB58HIWU5=t zX9z!O#I~l47lio0yzP&VU>?3|OZ}_7jyHlWf0GWa$4-*e-$Z~qJ50q@C@*4-??}7` zEgwkU0l(Z!YS}0Ii$YOz{d%-+>FZ#ul;pX{97Hp??s{MeDX9yD9X(q(3Ia5UM(gfy z9|Fy)HRo)=0!7=5F^JO)-V%1tQ5vfbf}!bSbj}4K=G}kQ)#i93!Q{CCiE{H6_9*Zg zS~tU;hzD$#*K(f35Ug0YOAS>;A5FcWqCf0;cWc2S9ZG58UfC+!LVHW@|B!k?h8Lai zJD%$hqm6XbTqYRu;&z@gXJ1-P^zsGQWrrt*YXto~pn;^>;dTqxa1C;yURH^I`H&TQ z6qqYw-;nPgXg~R9z+^K8?bzYF?w%ed5B>Mzs&4W)i&-BAAOE->L@PwRYU1k1RO7+Y zW13u}cOW3jlw-aXL?XNBWzSdBC^zlUf*XdEr{v;aq0G)EZskwn@`EJxD?^m)7Fg|Y z=t_`gj%Gv5NsL9bc=h%GQ4?K+@Jvyjj<=PtR@D4s1y;^jU0nnc8mFttHP)U7niw5&=d|`UZu_^Od>o@AbWtP!N@uOp0dHE#PO$BVS}A|!UzIeVOZgC z&wH7Lj>z?R?m-|%jXzzDUSIZmna7C<^L5AS2i$!xvs_PK+q;(Op_r5JZ)QeoztQOS z;k);^`l2O|Y#v?d7?AH-KQu?2IoR5vU?rD%r4}F;Atka6pAMaei)RfQ{mKo*JEkOF z2SA0A#BdW_#Cf4LB%WDOgltH*qA=@RnFTEI!>RGhpK{1`XXf*xkw$IdiV8gmnf#;b z%SBYI>CW|GremIp_*QvOKeE{KIS}B#ZGjG9qV76FFjuLLF4mg8pf57TR-(le@$RRT zW){4FA-aU!(cU8DMKqPFNy#71~yRYQHX<-z!$)TA9 z+Am$uUIs3o43T~W<;#@>UPQsYLBv;CXjOjhy0;b_QIul*QJovMPY?Xsi7~(M6?Xv+ zCBFf7_`L;>SsvfHk=6GQZ`r(u#6rP%5mn7EM#d`hh&H^Hw`m0dxUjPw#k zb-c<6UDpOEH?Yt=pi`iUZj$c=e-A!(m1}00$r~yr);8e-r$7h~)PY42JV}GV@Q?Hv&N%vREC={NXxM2QTkp zgI>}Lg+Cs?TjTZfjFR3-jWA{;@E?LP(CJX2Ov%dl4DGRcVVWMX6M(*TyBs`(?xvsc$AR24iv;eT(vqglBkc^d2dnD=fq(-?dDa+eu;Ee=V42zBOE8c}(b43?)B zv+sa-vteJHg0PL2YWqLT7}LNDpWnesD{YtpDmofFyt(^7rY`AQuh;{)fg9%!s`|#5 z@CwdB>c1}Qt%Wut`Qh;6GX7LQw&aH;D2~GSx`B%{u@W-;cN>;|UDFeepzk$223el~ zq}t1F7p?H_hgxY1Fd(0_y9M=vEY1|`gPzCRKnI_FNUd?v-c(}*XiU`SmdxP#dB8tL zMio8?0r8grbkAn8a3!~`TwOjFh$OO=k}hxD)}4KA=6i*FIp3Zd|EPK99;SF?PX1|f z3lZFYo0R;8H6G%V=Pc zx)`C6@}AM-;bq-RL^TDU4=wo`rps0CG4M2t0qS-@A6smUWLU%Zy^&jAic-5SJO_cq z;dgG}f+KqC4ni*|I@XYP7|K+0jUC*3n1(H)5MW*#^xU7|{56@HYnjTXakF6=H!-+gn@W7CV&kBN#pOf`!|E+laA=RQ0!x&?5-fJL%yC_PZ)q?{-)#WtJp9` z5MNzc$pzyr_!T1(s*Ls*!Imb_2LiL-4d8wELXEKHR()l*z;~NL+Orkj@L<#d2AcOj z!-lT!O>F;5Q2ynANG3LY_ZWh2m70EuCW2=sDJ#)(+5bVnV!66|40IALZyMTG3r_4a z{$E=%d-K}$z_q{%yZ48CjMy8@9>M_U5%?1=?*4x`y7G9azW0Cby|d3?jC~&>I}@@+ zt}T*;ge0c2RkD<%D0f6DEt58%l%{C4q+OO=QdFu56(vn+QCX(QzWna@_wT&UIdkrF z_UC!O-_QI&M&%-m#&=oIVE^8Lbu70NlEnU#%nnLR8Oy(3qosRZAXZntn3i=8Zu*jV zO;#A(3~DQ&C;Awr%~B1RVPg?kJkf>Nqek$($+dv!Mx9=9fRnoMDHx1TKUi;+w&?@2 zWht2b4+B_ZAW+5kXmejb24Z;+Mf~Mk7uKzt(L?vNro`mRR(Q$$Xz()cirX8w#emXc z499V}nEFXvU`z47E`wQOYqzMNv5Rhj_2a8Mnck0G+I_h9&rw^h$#iNe!>Sdk{J1n7 z;Kb)3<81WzJAsNZzPQN(+ifR&S3zpiL>1)W$^(#9yAS_jIq3_N|LMq17Z5AT<4RSat|&-M_fFXkPNakKNXmPu9@dySnYqVt zk9C|c97j7pwXCEL^PNtq)?!JRq8WJ*rXgL|eO!L76brChB}?8m9Dx#dcDhKd)@aYn zoH{za`UQ-&c7p?LpbzeRH>nQZFRd)xzbm?b1u6L?boq^ftC%V(#UD<*x8&bbi`s2$ z;{fY35Xm|6r?{Jt2Lm$zJnSUg7Hx?ws**p^{)qc<=LgFRA6d_oUCu^9)^X%26_i#3 z*4(@YvqO}7KZ9|_@EOA#BHVRG{40jL`d4eCF5UPL}v4p>NL6ZnH@yQXqfTzAuY3O+}=$f<^EPcR9JN#gN?iM_< z|6F}u2ddb7a@FcksU-eo+uO{Iq}O*_Y}a@&S6A3~>gAO9`ZDODnzwS(WDL$fI}a#)?;Q&J`>k$h>Y-hfHlFi2krM#9&AM+jRjrp} zM;U-*c#CpX^kBIj1A7aS!*4O!WQk3wmIYXrKKY&4JX&e0Fc&N|r@E=IB|jP;z@0|? zn%eN?0vhJ0BCJ!&u(|{cZ*y>(1m;)KI}H`EDfb}w*(6wf>%5DT=B8q7;MPIH32nJQ zi*%Nx!o36%R}Z8#A)yx+KwZ1z0Qh{9_q3e9)WqLX$t;5Q;JMjA@_Qvk!pr@2h#XYI z-g-nHHwCwyK_f%*giX+&k9YtSg1c#|oyH*2rY4 zV6CL=E!-f;EmS73i%Ii|g14|m%NIXLNS2XA2R0;MngWIf43tawJB+>#`=haTyet=k zb{vg^*yQmRvw=S^UKoLg8Z-{Om%$4thZ3yeG$JDug=`e1-x!|v9$R=Mg=xG&7W1U8&R|r&Do-?U~ zyEpt5V2v?CEo9m&V{Q#N(GRarvlR8mux+42U=O>5X$Ku3NiJI}`iEee*4+GBZP~Kt zd6-vgo3iCvQJ4yV+0PZxH0O+bbEt<0r!o2Cwp<+~kdCXwRsi~SFmA?eT-osZuLYif z+<9yq2TU9V3-%bq=esZgIjzMg)b=6V_wVTwtUtfkey297cO3Kg#}opRwZVBxBUk*t zTC8W8m(+2ENY)>d9=g+iJ*>%RG>JLhrd@1`MVzl7ccu`CyG@wCt<0gVpP1MizWi@^ zRwrYt=ZNiIk1tuK>lAF{N6rM`rvUTu z?5EnYtiWDSNRB@r%LQ??kT>PT7TH>sSp%GX6ojuJyYW|? zVvx4LqnJX=Y7N5{LhrvS(=h44@sHF3-&+^>{+QZ4ftP^z0L~F;o$OTymf|NFGar(X zK~@=Eto_RCAs4#77Q0dcZu-e29X^I1$M$_fIv>U!C?oUjRw;3_KUpot>3#6Ui!Vf| z3G|i}ceuIuQ^!gq?WfGT0SoLkFLZ%3jbzvmSM}M*7#knhuVz{)@%4U27M4@G^wDJ* z>JwaW*HLhD(uS=sF(?)>m*U!*WP{JQ_nM$qW(2wDW>94x(h@0q_h`1=t2%kn8#2%) zkiKORNJg|ivD$h_C1t?&oT5gR9Q)T%WG@(l`W;b?v+Btrn&=GWiBCKJ?$Ew(O6|w7 zx>-LrImyA>El-5U_5V}nvW|H@D#^7)xdB*SYnz$Q64VEpcF|e9(DN#Je^vkeJ-Id2Sv6z6ZTPN~|GAIO*_f9&dK1)MX5eTB|2G zVteNE%)H57`A#Tz0JgARcH3!1<{Tj*a)Ya+x%n+PMA0T$Euz!BT(6NNdGl6b9zVbN z4|s@O>8b2a-tA-P66gd`xX1#Fqlpg`Vjre57^ngGqKal(a)Y+Q#R%hm)`02f!+NP8 z`W(9ADK+<~-uE~C|>ikvB zRH@G!a6?t58{s)Ix_e*kZNCd*&xMm)(ye*d#<#{D?6$$@5mok!AyG2;yBa?vzkoNp z1xM0$<{^^4nG~cmv)Z=V~WtK8Z0zg8T~icbIeG}xdyrml!>WH%D=uC4LVB? zk94VeYW6~!a=6$Z8MmkmTc<3vf*h9b#Mb`*K=5cyYw>HrGQz&YNbwiDz3>O*PQ%Z; zF0}K`o+@n9uh2s)t5b(FRZG0g!Hz3u!)m}W?;>hTHq@`M?nP3BB4)NQf^qouBEbz+ zrEtKir&lrav7qpRd?TDb@`C$vy=u!K$?nnc!XPO36Khiw(%fn*bq^%GB# zTwAP;*+E*-TmX-s+ywXyRN+~AM$1(!51s!5TwRW8li@qo0zKVuxn|fk;GrCU3%<|Z zS$hKmH$F!Ddm=*as3$qCxi55(a9#O8_KVDeF_~}b+}0H6*J8jg1|!|-c?^F|#>g8b zbZmgXi78^L2zKobrRZ&7nnGMTkqc880_QEq?m~sV6QSvTIQ=>+h(4K^dTjkgzg)iaHLl6H|9s3!6Y|BVygHO>|$ZhIvcKC#>3 zj=f?*x}H&`Eq8%GxJpECEcYKw#&#SMM)#8WgHXSL685|K$VL-1@K;!x>wI42M*K;d z?OQfD!1Qm=(>*(Hg{rO-Eov7>_yc&8u`OEjDxf4C+z;01{i!M{AoopyY-QBzBHl5d zTa!|D5zgLjf+I1yQMMGR9&XqVgFq&BAM6VOAJo~{iwy@ELVhB(mx0x*jr37(iyLCG z=*=K<4U|MrYc^8rU-%J`tOUQ5CH)ItDqh`Ui4K26C9K=iQA6Ru=V)2HvRqfTVv51V zFA;y}8Tnxk{Jyx|PLa=b%{r6^Q4LyfiJKfvz3K21+8;^}o7H36!}jp^PZ|iiqd(x= z)^5G_Qu|1=1N{dy>5@6KYVC_9Zq)iJm{hh3Y$ow`fPW8V4;i59ur|prK{*IBeS9zb z^(Vs@`Dh~4x!MM-JqJg9l9grDt6%=enXdrxH7dW~hcYe6ALS15X_dDaRc?{<1Na`w zqDrb%89VniY2Tn;sS~z!c1xZ5sH$qRE`m~V3$SC+wq2U5P>;=x(!KWXkv~`L8l`6% z3RMgs;UM{H;{HHGsSNLnn+jhW9^@-y0o2zakoP4S-B_DCP_JEZTEE0y=b14OP+fYV zx0w8XH+%+vR#p5D1kt}%&~A@zD$t>tD?mBW$C&-3srTNntG6VrLYkrD$P#X|9S{#^iayCdPOp4N!^dlz&pp> zdyJ}!KImD3h8ET#$jwJzq(jg0boV&(@V3zPjxN?xt0Lv3b$!Qc07n)? zY*s?+xhSF2vuq8SkWuVx^K}t_6Dk+}R|m?eB3pD1JB^x_tVdG(2}=&6IYkyUu_-o@ zryGQnj1wfV|L!T8>;jIXZ=7&L$>SUjfjlBqMs~d&BzvqTi@h-wFh5`UVhA{9a+iKSrbu;AA;=Rr4J`lf~DJi6#f$Walfuh}TUm5?Mdbzo+ zJIJ5lqs}jvPfj$~Poo&2ZxI1;Bu`Gdit4Q3qzjL}KtXZbxJyi_8gYvKn{r^V0|^c) z4fOhQ>$EKbAYzOs+=M|ON)xMA#HA6~PD}p3sn3hyO7AwWL)XD}FZK#cR0JoLkVI_Z zxtmYmpkVB(KY;=K&xC6`0w%(dqYHcfWfsc= zQwRKJ<5G#75RmyR%e8RFGbBZR$5Z#&Z{q!un@_^(UG&Mk-{i^!n*22oUd60DVhb_>K_*U~q?GRX*e2%0c_>0r2K02?zgIixTGQ@)nR*|f3su=(y$q5k zG_mhc(fDrBXu+Ki?sag`2W9Q)Mo9z~N|rQ5bNk``6C9A{tQvm34;p#3->Wnqt!K4r zpZ<^=47pum&<;DH{LvV8G@yH9u>aV7=-onnEfB66^cTDtL!98F<}m{JQosk=JF4^+r}XCz%KgPW9;Bo}nxdn7W*O_pV!s#9SruEf+@h_;3U_}x@)D2x{< zqOURSWgOLLMacy@P?nrv6|Z58RqK_k=|VKu5Kp{Zi5tBVS_WL9Di^f%2UJlI1ReVY zO;7ybzn||<3#}FiRx5F}p=vRuiFgFMLaKEUmqFb{ZLY=i*0_;$Poc})J;E1WKb)O&}Wg6avZZi}2Dd*W>vDaxMSYgw?AvTN<4xlAYGM!b1ej8=NI6wgMwckdlP6 zLrL0rJ~OX&v+Zdk^r?v`ul|%1#wYbPZ9&hFPV_cnqV~uxq)DBe_W4Jq+13XK?pUAb zr=$+-!hUe6_n`Z4YnYX~YiYMaf(^IfNTQVu%(CI_a$|v1ON(xWUCBbGEC>e&hAft8i{Ym%i{+&T|c*h*evX zR$G3gkbaSZM?pw%*#A2ATs&bJ1MTadg0eK_Dk=Gd_|I@TuJN{g33sW**{}okcoJAHfGpV2=KgkZpX@KBT65`d9Py27XSyH=9CweZD4 zvN_13=FUvSf>-rRpxkvwVAgUaQO5v(CnE7s0a}28*~dZGQ<10i@wvG};)!>r*casM zHjI>=tcMi5g8JW6zjQmF`~g{EeEm7qo4rn!A3@G0D{)&)uv$oz!#%8SiW98! zO9bouV6T!6Ez9j=-;uRUG+0*%U*FL`)0pa;`5?id_XR`+i&K-b6^ zU557NuISL~Te9YyRPt$4@T2mdXqJKoy;e0p*k{&9Y8r(ZfpDuSrM)RjG3JwI!( zYJ5oE<>7K)-xeiw_ab}5r8)+_QOE;PA=6s8>{X~B3Gg9*=wMj2pd~9W^RUf)qqFeW z?K`nUljqRci6nQ5=s4Ve_@AdppcNAcB!52Xi&WnrPEunWPVVcSB{8rwRB2-@r1pSJ z#5k9$V*33ss)C;Y7&*6t$^Sr*+W#F-w}XsKvA5Uw9<@r9pP?d-0&6hw3P=vdv_6cT z`3bWtLGG#lP?fpr|JIyE^%-0j&exwopa*GXE3lr-Zs%{?f@UeB?sn|tKQ~T>yn6Da z+5}x33Qg9kMKuBa^|BxkxWkw-SBxkk2KeJlo$vvyw4xIp{eYVE=f<(A4Ok~k2;;PP`~@*A)a@Vv^(~Mf*^5oEw)F&uxF=%`Y=K7Ah*O7(9i0|bl!Ct93vPKt*sEKl*c^kIHUOE`X$^M6;rI5Ds{?mw0{=F zTYntl?mZAnX|xs5Pi>UsDT<6%AOwSSDDIH%x^}F#6}f7R&JTsmtN=YtHL6z|(}nnE z*j7v86OOi&Uux%&{r>{zZrO(kZ9qRtQFSwj36(7drynyUU+N7{6kRgjZ~~tBb%y-a z02XbulX8ATWeWh^o@NCWVtbzif;*t)&Bc7;Ge05f8?iAE5@agqng1QKYMUa23A+uS zsCMg&IRHm`t}&*o{9bUhUhy`e5m5bK`td<*J{nB#_yJkzDG<_~RirpZ5?IuG4wg8B z4Xv5g@)u+!Um`6JCq8<#Q9n>ik^$b2!9AP}7--t9sA>qkSdc#NAY3*6&U^3$4>v~j zO?+eo)|0E&r`k#LoFRH>50}sTpeOs zj-SptIOl}rWxha1kABwahq!+~7yhPD{75?k<+v*li_ej+21)x0=IMUG?5X@CUJpA% z%8^#ieoBP7?Cp|cvwoy##ou7X0oQz~tt7a23kog^2w9C&-Zzb-H3UFo= ze)x)A%nF1$HHIv3S*9j-L+NVLyq3rH77v{?l!X+=M?wLkCtnK1b>d70&3O+LL$RaMpv0snr$ zPC1VWW~aphhV(>%0o@p_q3{=zaYs=LNzs)yeKWJ`as7OC)maYNNL30J%fywU`v?}1 zZp_lg<)}B5x<&#jWmO9(p+Dz-oc6hNX(wmp!;Hnh_aDxvP+@O02~D@e_c6H9NtxYL z0fjFs5t3ny>P9TYTjQ4H9bqcial_lc?{1n7!rsni_-@nYo|COn5jMy0TgG5Xb6&Mt zlq*@N|JCT~p8$zW3_=1ojb?QgMn-^JT4eBS700ksN({}_F*xJ%tJR>NYP1tDO)z~r z)%9M&&2{XnkK9zqm?LKXC(@i?voh!GyDli{(8uA=uNIiE1fx#a9k%eDgM8C9Y=Uqe z?SMv`>Sd}v$Wn15Od5Dq=}+YDV;7Sc;7&}hkp5M@U}~5|18ujj@wdxKdoswvTc(hg z>;0j-7i2m=hX~0=v#YR+UE`4^S8Q8i=2l{6)7QH!pa;)>jgNPsiupu%Q=9v#cqh0g zi_OjG+p^~@Ksgk89A;ldXhyeI<5zw(soplu9tPeI{Q56-glkbV9!8pgjlqy!y^6%x z2y6nU$({a?{(&TrN(a|mPOXA)ms$n-kj`f?un#J1UsFU>zC% z;9)1uk_SF|^Pw$ORQ%SRL+f5K@xM+Bmi&u;)@(j$@E3TOu0Jl*a#DFemh1HP*IdX{uU9t=EmK!9+Os}A|Xy*$j>YqBwXL-Iep=v?=!pk2LM5YnN9X6QB@-;SJT=Xp13qEp;l1q|iR27r#+ z;A_hMe=*mZ^@Qm&gTtOtx$bK$`MyIQLPKjvO%ddAz*c*CV{9Avh>c$%H7(9UTtD6d zQFW__0+#ZRQ09mf5S}H?+IS-J{=%oD|DlCMg*4u*B@#fDkVG19R$i%CTi721yY0l% zugu(etG=ro53hSr<;ajScuF6Ul6kL?um8Gfw@e`J{^L>}jfO8a1Q0rX{>-5Y&a}xNv5;yONOl5vEM;a+> zC^4(r1{(O&D|xf-X_(o}cMb>wx^{^@PB$Ye0(6CusyS6R>MJ_lF20faMf>6B8y=9p zHrksbG=-VMcfC+q6+A)!6gpj4uq;kT*>9Gf2Q7-I6~|~LCX-Nc@7cGzVYdS}){efu z8g%Av*v`7Uj#gG%{c?SwuL`bzP1n`cEA?;x16K6=fUBTsj!Wx@ak~pKoN20M_YyWW`!?y+`$YRKs^GLOzb$|W)-QYj?EI;=QId^{6Wktg*{f~GS zfda4Bh}#TdTkX|U1XmGzKTTHrbm<5zEW2k0)Ls)vz~0xdvi~r!Z34$wIFmi)-NF^$ zbDugQPkprQ(8SVzZd?wN%y&j5huJwVxHCiK&ecGq*+txoV4ip>bQV+wJs3ZK!U%if zBU%2<46TQax@mzAl`9^qMnyvy2Kw`MGsH!t6d3*qTO1G}T%g*CW$h z>7*dNw-e?(rxF^we*&Yq^LW6@WEL1Pc=*z%xJjrEhGUZ6r&P=CY2nCAeE4K3Q#ln zvjRT`om)WRuNF1esILDV2t!t$*h>?xceM(x2(>;Irgv$%ek#w(BIboP_M+y1(sz}ld3WK_4P-Ypn4VYyE|s~9KhHbA$WXjlQ$@2&*MOwuav79 zT^y(Kh)YYRedt}SmLp^josY}Zzr!7Bc&`=8T}vLh?MDgtr?XhHIhl+eEFQN8dhkmc zpT(ZjQ$zJhoum3lCxa!vqzI&MX25X=TpZx z#z^}4d9*H)#1TZ4$@1F`cdU?a$oNu4$@QA78$UlOb)WAe10;?OM}~RsgGiMJHUaA9<(ny- zx$q>j1{C<2cnQs2p*{OFC8x?beQS^RusKOPNOwE5T!nCpmW)Xy@J{XoGJNI>pl%II zp43Mv0SCZbQ)4JLzic|bD>7-J*_(LD(e=&s+dHv!BJ*`BqD?2Y_m+l4tKtC~GU^WS zfXyCk+1jU?_&Gg1sU60-QgaQ2^98^G!i>E1l&Zx67A=02iQ zKBLu=8h6c?YWP0um<^HI!3c`eNU8{SjykD9$YrSE#|oLm4HBNn!Czzz(hoU8Gbg5q z!xgxly7rvz^%!LHlJdTdB{4M>49;~xq*NfnytcBWA55H%yjx1g zCe2W9!jRyLiwxp_wUyjC7wg65evIb}x7a4J0*latU;F@YgG1ejd;~p9=#uPJ>sb{Q z?$aygQN^!-h*VM}J8JU{>~a#&)G_&&9oJ}8S`JXQ8ZKX2s~Ua|J(D2ueDIXJ=oIue z&Cnf#=6mBIEl|SYqDAbPm8I}xx27Z_-YzMSZMFyDH7?DE`j0w8SdeN58B*8@v%@}+3Le!`Mme?=5mP896`Y2zr%O=h-YlFi6?jS6lHrDBuv+~U|=nI0b z{of|lTyd#Q9#x$)allO;f$FVb(OOwao+|#P+MxCM6%EyIbMoTH_1o`+6+TKN9kjxt zOMSV2ET4m0R+#l1JS-VwvPI9#;+33>mg7X=57fwTGRVhjNLzGcfFJT7}O1RGOHvsn-(h@ap?m8^^ z??-&r9?5v3Hutt@qONaDAm(5k^sas{mdO3}Mh$HaM-!`SvbwCeSJpNNqGa)fq>P)w z^$(8BS#@4EFMb5R6;SkGiN76zU5N9C9~a>j`KFy8(-u{+>ww4b+;RIm;dU2Doqo!& z=GCBAp$53Z{K>nHS)p1I!SrJxEp=2AU*1QlP9_epYik>xZG+17DSi7{SnS-ZC7P=f zgf-lj9a%^GkX6@J$~cnkHRtw0Z?^@)TNZ7X#o1_)kLZdqm5{vMa3s?QF!RP2VoAEh zCdGOLEZ=WYqg0)_$w*qCf@wb|9;#TRcx{6fOZ;O5Uc5Rj`OeaNSsNm$=#H!b?i$x0 zW1-)Bw=X&ZrL1^g=*$@rtaxNC9i~n8y zJjH3_ed`7IuKs;&2$IU~M{<=~ViKsOtDyBRSm|RqNdP5R1%?T@wRvu)=2+&Jq_bwE zu%?Hy;1vA!^oudrC2_y%y>?T<@AoO7h2Gz3#b5q;lj?IQA;Xfp*R->}kM?WBow=jE zQMYaP@6qx0Akc^!29@x`jYtBE02Y)={XY99hg80bXRPXBfBAJCmh5ox-$5?^Umdr* zS(&=*hS5qA?-OI3fHj-&!z%byD)~*|aiQ$nQqnzJD_o^y)CDYr-^xnPlNoh*il#Wt zP~3b^4=IVKI$n!Gpj)1p91E9+QU)2p^(t)d?a1r*nX_Ly7b{#Ac(tR{&EFRZ_H1{2 z?JMp&yL0{poBD}x7q2NR2Jk72RjXb_LsJYL9c|A1g z<@Tqikg`WtQq&|yiL|9IT4(1f_(Pv!mw1Vf#wzbQ``)hp9SQFApz)8T36|TzP6KQn zNrZ#ne~)ng9oMbY=8l!F`?U&?9$wkvs9H0v^Eupxnf;Nj84b2&16Cws^!mIQFmv2> zA_RK4gaW?)rccLvOt1M-m<>{k&yz3iMt#8W6w3~-?Mu-&;(HR_l69)w^AV`se^9tQ z`D=4LVnW3@+%V5&Nk5f26WQ?)_AD8B=9Z*47V-3?ReXU?*;G z0F;(Yc?;nOyzl5<1oAf(eKvLN*#QRD!AP0DL)P0~%l(aW@zaaSWW~!~Lj8AG;^jR` ztR%8A8ZR#~SSu?NMU$MP|BcL^xuBprku3ElaY>cpP}^F$@pXzb$;LSQqRw^jCmhv7 zDwj24px^=k+4Wi*7!fN6$m7#muG8cDw|w5wWJM4TD_LxPg6A_dJGEaS-M5NmZ!6Pb zLo{>|$=!hU`$$Zz)tZb1a#2y>Lrh4{UpoGShn@GM*Piqzr)_maxdp7rBRw-z(OTR@ zjf`fh#CZ^Yc#Rh`r(UPI%{w2BrS{YTYLDS}c-P3WRq&QoE|{f1yzoKR(6c_!5J?$d z=R2_?IqkgKVr~*F5A$l|#_u2!E1btjKLu4BRON3kR!?(SV<&#OicU(}Gk0l_oW`Q2 z;e?JOXIyqnI<(|$mt7+6BhK^_g}oWE#S3N0r6k$*9mu<-;$hz8#~rq^)T>9xooY^k ze)8v~aMFX`2KLmg_6BW3Dy@;)!=;XI^q*XjEJ%S~a50zuwa$48uBuC%>JMC`xb*&K z9hTb}XJ8ac+j_F+fN}-@^Le(1S(yjhES0WXxrUrql3Ns(MkyUOYjR_|$}tJr{xtbz zO`*dMYwM|(@wQn*t-aq~y6_Tvj{ewSUI<~d%Dizk$=Kd*ZPDpnGrLxz^&(sLPe^eC zx_<~_*$G}tC+-Wiu^RI5yQ3?hEi;?B7k=w_^|rV&ituS!S`hjr7W+nBJz}MV#sk(U zxPUWgtyi%58Gk2$RtBSmn#v;S%ish^>$Ga)6qx;I*-fIao!e*MZr_xKrZ5OZY`+6S zyALJ|Ks#mh0-Q~o*V3)mG`7tYpj+v=EK^XVk>>Rpxnn)Iaq|e6+c*-U{nEBmDQTA> zq|H{u4u6PIl^5-4oOgJI9B7#@9#IzSc&T<|ulQRNzn`76%71pD^xlNIsC)rgxOq;k zqW}HPAAKEjiS#q`VVfiAC4YVowd>5f8o82ZAh*#K4rJ0#PSRZ1%B76ae~HksjpR7p zZ17DR7?N-j9)i(xx6JLYsx^H{F>Hr?Z)Zy>$sxP~=!7$x=(`vt9`v5(SsBFRQFMs= zM;5rG#jtw_m@6y}{s&I9xEQw%ez=vXa9|$3$!+||?vAw$5sST{*OZ=bnT!QlZW*kW zF~*WzU|=)pd$TO=*5ZsbwC5;(7`R&R6l_M?T@)6j+u1B$&Z@| zI*#eO#>nylsF9X0jPIN*2jvT#vpk~x|1R=`^_ym1eThH!Eo;ln++IkbEY=z*kcLV* zpI|p8cL3VeR*Tha7y;voc(J?Z0shJX&i55hOsfpE;K4)f2#8D0w61F7Kj617I-m%O zWCC4&zkYJD3@GlPHT1$kU7uAAD-WcRHf}-|Z0EJX#LogXZ1A1Sqi>;1?* z17~U{Pq$S7=h;d58PR}a|CcU!N9k!WMQhAAkOG`%%P&4N z6W9gN$m1F^Nhz-`Jc&MPuN_N*zHLI>hmE=aB!@HLMUKxz`?rQAl?p?oS|wDrs;i5) z_MH8Zkg=OE>c+GWhsz3kn!cj9wYR6ZIJcNX$6v|Vd?HOAe@i*DC};f%(a9g$8^iUs zlQ6h)o5oe>dedQ7wTNY(G7|%5!38HPBU+jQFW`!Qg5vzcvOd?~l4=mH)g3$l8LgOG zU|}mOP!^Kpq(jO{&yP7Ko3Qy40#>wRb~fdX1a%6NC=(S z@GZ|v=k)ccX}s+xdWaQnP(k&nv~N)cgGyzmsGrR5P-)B<%EG(WdmSHqZn&n5@>q(s zS!>$f$>|($XQOA-mYkSy$;#UvL-n`@^ozLZt0C?yQts=u=naAMIyUbI>6wu$G_CMb zFt%_8%J8HcO)29m_rBKV*1+SgFUQ=EbW!ri^X?$iHKeQgu`uhV4Nrn}zv)Isj&)RS zZ4zWKr1CXM7hu|HuwgO!&^?#@yq?rtxht;<`faVQd7M1&tWi5M0 zE*OHo3ODba=b4%mHOP753H@3wp_*1_;yEU|*4QCyo+;?~1~fBd(HUqL`G(-jQl6Ib zpssrkD8kFK_XUQ9r-}+p^eJYM;sg z1RmVskB@q>VBrQ)piagzC5^XjMezH#DRbCF*bL2Boy~WS%0`r04!TQBPEj3f*VQ6M zVnp@Herg;fl@FGV7viU^7LbOZqBZR^h~FFUrBKL!U}dX{)Vnyf?Kb^mj=US#K?N=C zu#G9One)6WSUww)EHh#~k3e-NrDJc+j~q+fBeNJIx1MKNSQ&H0*jxW!09THu{B1CE zAZ?X$uq62TFKCG(B%-tTgA9ef=Q6NwI(ACdblld8+-WmySWy0m!9Uo+05MpTa@npo zw`E>Y9!-1-*E$r5JaF|^yAQTUV2;*CZRg=Ia=yDu{aE~?t~4^weKVPDnssHpvww8Y zZP{f=f_!etzT`tc6d6I>Y+0;ne`$E5;#q!&+1VA4`A!AVbwE75=IDO!)ECjZ1kvrT z7@hxt1uza*4c_GseHEuLWR|PCx{6X6l<^IQ`&x_}d}^e@escVu!(+7$p-}vp2(L1P z@QoB?*_7!bMpzEromwLE(9q4bU6)R=E^PAavj`NTPKs z-xh4P{Rxp!I^NMB{qKY=&wnjac&ipz$XGmK92rj>fGhvj{_Z+7;j=}h6KZQA>p1=elIP@JCS29ou>ZpwuUrpF<(~Zt4mzMBpu2zEb}v zwiLn^5mAV4g3&nmSCq|Q`?$MXQFK>t;fJY+TWz5AWGT}Zs#+m=_QCVjIwiv$@A8BA zZU?vr58>|<3cD~)?d;l0c;xeyxCr2sIirn_$*;!N$w@l~W^UWBy|%DG7VDGYcEQp# z@D4r%-h+SIl7ywtxVm<{qw@X*@`~lx_r6(+t(m45d|G>LsMrZ&S;L=Y{cptI4^lk4 zKbCooAvRs9p(>4R6IV_(vbp0VpJfEX;p#!W-Ei=EB>uC2gqpLtd+*}pGhOLKRN;pc z6*_u4;#56dbLW(xlF@pT{@}n_g4mv6B_juH6~ zTuU}8hO_Pd68?uq#;K36^sE1Na#DG{x=JSr`0&^7k#HP&O{CikMe~qrl~p{Wi>$^m zBGI;kcx+h!<-xn7pPRxjQ7+)^DT#o!xiRsER1QM-)_PW&Bk#=r|g;sk7$(;J*#_cb#J(?Q@rX zkeA-@(I@fvKm{4USv7q#luWyu@4j5k^?L?(K4I`(gJvZ-a0WVf8ed*P=Ak=&j$NVu zgyHh#{Nb>G8P_ovB#cbtqKYHsA0~GEsQ(1({(SY%mJ`b_ z!)vZ2o^5RgD)+3d{ZG;C-T(I|<}~>7XHICjy`xB1`E=x-z!2==E(R^HbZ>M_!=_A- zHvQ`3%U7&nF=z@#M+`wQc&ncU#3V;g%%SL}9#*nX=>yVo0L`4+|ASoB9Lc(J|B(#4 z6{SCcj656uDB}lU=DOsGF}~&&K5tZ!8<^FzJ2LfXJ^Q=e5P=5l>+7&x|DVLi6^v&! zt-%Ifs)&rLZ)Da@XK#U%i^b>;me6xw()7Ba{_XE`iQGU&P4n$cht|g`G*dL@lv+;i z4d<_;mw0>U?tAa4ZDc95D9;O04z)lpW+`&RN}Svn#2v$W+m6G_Ly>z5H^+)@yjLlx zv5tM&I{i=;9Xx+#c_0XW?%6|0_^XPC%2$eJaBnE)Yv0IZ+MYtUVrSRad$3Q2E=PB1 z@toJ>gOb~C$lL4$-5V&o++(%tov%1q_H4mLg3pqU_I`q2w=-~Yxk zJ%)0vvVIr#mJS;}1d)#_uT;6XUzi)}42*}rMoab;AqTlPt6Cz!O~j-Rn-laL-7`0G z=R@6Md;7(gRRmR<2d1}umrA7)Ny!H-*2_s3Y0aEYzvWL7-mzB{Q2XUE*G8V3iU-#r z*kK!`!%%SBl1Su00-0ihQ}2F+aJOGY7nS)IFE(J`S3rl{O)yhqa2F0B0n@IKTU1fO zivQ}E_UrYyE1x~zB$8uzf=adp7}wx*>6|_v8x3oO1XLv zb1Ud*J;>W??sTqK5m-a`{9iJU-NC|Twt^*FXqSWei;ZBO0x(-2Vg@o;WhZX7A~wOK zs+>w1#sd8pjLFzyO3XQGToCnyo~YFm&)*-xt<1JfILWQ7F6WGo%0M57VSjn_K72_5=&}p^>c0Ka7E2X{AL;nO z9yE+&VTImCu!ar|&AB#61moA8`LCy-pJduWkaLsEw@Ht%NBoyRc&h?@v!a0syh+Iz z&aj6C4>CsI zMlkkI8K1hGl|TfJWWm0G#BrY-7|$9|#?ynd|K>|@xvv{6XsS!x4-@k6Ue#LE&E0593vH0qHCdCazBf8$x%Mj7M|Lg3^|Dj&r z_j_i{7+W)0LXi=fESW4N>kN^lljT&RQKLmEg;1z?52;9)qLeI?r5O>9h&mYSq2ySP zh|Jit&X5>mX5L@teEx&){loQoJ@*gy^SXa{o?q_ky6#*gsTrK`>T2Cp1*ai^^E5t2 zt^QNxlSjZSc)S<(wd38(hg3FuM`=M&mExw=ZHAXb@TSys^X=4cl^9EDW*q?XQ`gfH z)p3|M0C@$4nzK#ga?cq8%`aA+gqJ3zfn_Mcn5ri0qx-;|`tHFX2}|iGKYE&22@UOKvDq zF>tG~rM~BsvD)TP|5@&#M)OXTd=r zPKx80>eMAed$SxI%!5a4$tPj8GSb9zNZ+U1=Ri{cKy)4ZtszmhN;{f*b_<7u28>2~ z-;c+Mh;N~@h^}5ICnes+3Ta-}(KHxtOT~`lzoGO&$W1_j37{}Cyil4xSnqo%9-#>| z$Ef3J&<%0Uv`_=ZnGph3s%bJeC0@w_Mx5Tj=i***N{GeA9~HC)2t4IR|AOk*yXA&U zhyCdUuMfRgYdcDO1U;SKJ_g{eIe2YvK}mz{ci16p1wS zj;X+M-YNa)Fy0!Bzyqd9H=0L-R#|@$hroE~WXNu}xUii2*59Q*eyt=)l5Od>ar$~k z!UgNlgTV+R&5e|P2n(G^LbOP)s0TI`0q5G z=#V%E@lx_3`uUGDbQ|S}R@HjvZZtK~ca$6a>ubA)5%hBzv&LrHAlmNnOynf+AJ81P z_(mB(x#Jl$ZQI&9X!C0LwABtcNly z{K-?E<2p35lBQS}eRiTNCSAHNDUk>$59zj$`mAV|_~5u+o%_3i=nl_V_7&Duq016} zfj|ZmcXCC;f0Gf{#Mk-92$P37etj=imt9GI)T2lPDecxh%`E_5wIML)6z}EoC3Z@u zsNk1I>QA$UHcn92UlS~&GIgdd-Ey`}R^^y3#@&D$Aj;Kv&LPx{*bt)&zt{Hx%k|_7wcpiOTb~|Fi%J#g z2;Y5k>vn5l5!I6oghp#Y+G%k9@d%tFJa4puVO}3bm4*dK{%{3iKA%6 z1pp6gSx=rw+iB*Z$Zgz5_26+8B!>1SYdsY7HQG!ccJB^qAQVvUL0`rm1JKO%b?CmZi{|9xUUIno-m{m5Au|wUogEsO{Pg{!WWx!g^`6d^#DU?V zD$7TVFHQRpVYuoq{*t_oP4P>nl-M0^eT>eK+$qaj7YL?MdZX=MQ#cCLi&*ZhNB5hc zDbb%hHDcW+wD|DAz`E0i#w2-f2-%NYw?)_RdV4yfz&hQBJv~JA0%#>tMKU&Fi`PUK zj1aJZI%nxiCKV%IB387NTT{`N{WSoRol6*;?xB3Wylh9 zaugbL0`%*)Ph7;MZLX#-N9H5#nXrR&f}=FcD$n(x$+@XuUFm*Q>a}jS3lx1~r;z_w zN)Ompy~LOyiW}JGFeQ2~FbiZ#UU(34U}$NQvttiTbeKMb9oUY}ezBjxlNgzEh)qJ` zq`AL%&T^7eG<)5&4iUuqE(VTza|)1+72*Ow!|F4618HLaN&kKpE`Px7o- z!QUPyL~6ZXo=MVX=LQX?4|Zw@re&nT8*Thh_CGGKA<+^ZyoA&#>V@gg0)ePE-X2#Q z6}uXIHwF7waITVHOt_dI2~G7G4HkCU-jiED+&6Ji(YdXjf3xtNQG6+Lw#)2GhHLeM zl0V3|)mj6`&qf)|c|5_)*4)1FY%!vfz-2uBsqLK-9&9H!HJE4<`Tgz2?KYb@@ZHea zuk817`Jj9N#3x4O;L8v-%JGPp_qp2!oHyrjd+B>&=>7C0+gush4u@N@KA$a;zB+&G zRjq|P1lpzV%RG%2F&wh8LmJ!)t|J~_KP87EKAN+jn+4w4-brX4N1L9PI>Sxcozrg9 zA9ZcLX@0tdby>$)xLcIR%Q5PM{WO<@Wub#DsU|U9@ZS;TsA9DI3G5Du>iQzd;kKDq zs<9)lk9h{euT2HirVo>sbp?m8q3ghX{V&~u>uF)x91Ek25P0!h!C4GAMn}w zT2wJVf|WzqvwqJW!sSsUqqju|2l;Fe(5|nK#(Y>sn4L4BihLt!%$r0;n|P(81Id0VKx{3hy0F;t&Vr#QPAM?UcCrm2PxPrRuKXtWH`SRD8cyH~=fd4ttf{ODZ@{uSuY zg|Fq84C8{^(ZhGSdOG-JAuafGHAfA0)Y~wDGO+_FBs^5@#b-HxSPv8s+#{uQ_ODv>Q>3rgA-&A9>cm@1GB;1^E}*@^#x{Jc@)qY(d1m= zY_D^$uXi%-!!#zuz1R5Ds^A7^`w_f4^!R`?`^h9bJ^#chd@>|dd1-nyFFtmQV`l>5 z`|*%ddso-4|I!A1_*_%WWx4o`#>2L}O#coqO;anoM?aK{e-xODQB}1{L$3}Gjk>C1 z)}{vvN6%C_C!V)~^bGTUoi=>#GBz4hkiFPlJqZ1sty+M?Hjxyip4CDLS*`-Q-P#d% z;?y;ZqdgS?-g}v&!Pxk2e$uXzq)SE5ugsY=>V-CGUy1T4`~1m+aJJwn!$0S46~-WZ zeK^fHu7=*XbE-J&^Z;Oj_WeG?*qA=+RZ>{@<60hL!ACwBqEa!|E+`mmUHW|^nVrq a2it#tsAIaS9p3lv(b>V(zQoQq_WuBm=|rLc literal 53345 zcmeEu`9IX}*Z*q7S8?)!V+|H1u}@tD`Oo%1~BJkPn#YhJ3WD$*Z1c?g0adZk-8G$4o? ze58gD2f!aRyWmp@f(2M#zpk!y{W{kpCr3+bI|~T%Nb(JrzEzJp@tlx(Tj3o04A)55 z8%aInvBzCH{PNG#1xuz(ohCW2Sl`kxJVn#v|5P?eo0i4Oh~1DzZl3llzCr1S!tHHg zfsz*sA)_2aTSKu`Ba<1BAj##|IpvR)4<@}^$* zV#v=gEuL82 zY_&V9bc*-*9ix1gi-L(K7p^>3#=EqzIC119m0z{ROWG15u@?+GbYm)2ZK(y$KR9^w z27h+C|2ey_);&re!rm+egra|~+!@no(uSRGgxhp*Rr$@!t`8dIT@{_=ea3_$1&#Uy z!`~RC;U6WD%o?qeq0d&sw@K@^DyFNE%QqE+*kR%J!y}*41-$FVk6c(bZ_1pxEvGO4 z0a2a$Lwx+nXM;3!>$JhvsVoLmj8}9m?8uQPAC6iSZBt>NDdps8>Xpa8TTIk6yl#!V4SOQ`uzOG1G%VG z-B&N*=ip+m45_K_zE`KtxOE2+77%gUt9`P{s((ARSDH`4-pZ{#-YPXdxqWNJYwis3 z+SW|{N^{%N*4&I;G(Z9bVTyGvyx@BuQL_7gAs)=${}2Yjwf}(&j@bY30{Xv4fXx0+ zE)+QY9~dZ7*aJhDMgJzbHRw6ps+?{+ z2R8`f3ccBTys*5&)g^ruCO!53b}8>p-$}gA+OB7gh2H9)%tKs%5D?~4y6QX={yrMN zs%@`BN?nT@kXi+U6KpID=4F#qO9MSL*-dn3Uk|N*ve09@b43>7ML|$Noa9}pXFJ>x zg3D{u(yEsw1t17{vyF|0G-+>;Q}f(88pCv;y4%~Qxj4W4j;u7~#|1(1^2wOw_I@da z-SAxYH+{6cYrQ@|9!8lfWeh7js9B{x{BK@-isq~y@u8m1_wUOPGl)6}epe^bI>JUO zE}0R6>aw+O&$TFI8RhmJC?$H{c6w4*A7f!Jc2%6~ClK6GO+FOSc3q~hk55}l*@;gH zXx3?b;Nzd=p$S9w`D)3bygp7LoW7ZhCF&~@f_T$ZU6{Iln>Qz~t0H%2VG!n_)6lwy ziDQ-AY&ba0KvrK(01V^D%#Ar)6T2prO3eC03qdDx<(&w2VX-Ji!hl-j$rpwjZPZpWB!_bQp2k?b3vN5if0`P?Fmx%| zc)PCW+Yoca250Pm1m#;d;!uR1!-YCq`BM0W?C>HiL{z} zcHldZUY%oGYb5qvpMni(MXlNB*nF6U-ocNZ@GmbRh(AJdL-}r?NRC^fO(eY<*fZCm zq)^1N(XsccvyDky!^9Yig3D4uT!uhu@EoRAhpBlBeB0fEp?MdukGCzJmnXb)eecY+ z@1}+H+=~$<%qL;gfl{tzGvUG`s+PZRoRr1wJsYN(-WQ+it25h^qTa}+?ft9xeE&=d zW|#+pp1)D$X&c;ummzDH|L@9bn$o%`FT3Jqczqb;-Ncz`ROyDZI(Vu3?ZyelKpn(Tf#eHSx{AhcuDAFYP2<@%19 zO|8@5ijOCzAcw`lW5-tfXY;Lzmv?2u3=`9SmxD z?iKD<+eQ+@51H+W1ySnN?0bGV-6qsC#6nMmG94`(3P|y~>t>t=0N9YL&9*1zmcXS0 zol7>@4LVnsIff1kFZsHMNQkSeD`;U0|62#YdSPFFyjhl#CZ(EPLraFZVh(X>Zqp0| zguFe~P_D`@tU1fm?E8$ey|m85cn<9?b>itPlaJ@~`hlDuFhX1|T|sj)Q;*7Rw&GJL zkMT5U%%ZiuQ~M%Q?(20Mb~3?V$w8P7b&=vWxH=oEB zpBFUql1Fiabq`H7xW1Cy)+uFDX{Q16g$=N2=;pa4_2s2l=yjh1^HqaG%y-dESw~d; zAW(NY1FMD(NZc&-v}xZ20-n1l&00qfgWz{+UUX;IujKkR?`aES8P(d7Ek#Q!vt#e; z>HGxy{|#me>k1;eO+8Ar2~6DEzd9GuPn$NcW@ZxDB$&$d0vw_a@%nsCgMCihK4G#F ztOhAa%9r+ZrL1bkSV&uYT|%Kj7QnnBKj~A@(yo;!wkKE9fU`NP*?s-S5A?c4Gx?~N z0m8YmSsEo>EA&b~(yLJpH~&=A)~H~$aw~6CBF4fsJ_U(_pwX_N5f|6>k2W60McqobVaTqT z?>UeA)Vyqtl%|IG1y&vqnYVlH%t>hgQDd?nuhxs_`YtHDy7*&)Cl7KzN2Kv*_c_d`I#QY-$YuwjYekZV8D9pA z07GBDU86pmbwn*!Y-oQYVNMQjcLMqhH~~=ED7%$|AVM}v_F&ggHz< z=lk>1DnKZnTo6X2i>^~OqN<;)wGZT$uV;XjdC#sH#FTac1%P04LUhHyq-IQbi_p8j z&ZW@-t*sA3NYm2EX08DQbm838GycTexnlKyzc0Ey3u+hsT3f8xefLdDAaz93FpJ)Lr}iVx87 zl@))@H%Hua+_LwLfYCY52CeSB7^)u=Ehfzx0G!p$5x!jCuH`e}sF)X$3Z=H%Tc0Lh zQo@=#bAOqLdf<{uJ8-ekiwZjYkWajYo_Kr;;tQZKG@2q8`10yqGl>*y=9=cS39TI{ z&JR>pqyCM8F#g#rF;DTo8FIA~_TY{=Z{_G$-j~N|uh$&}Bs~mFf^WtDk`X(HPQj0P zEieakxGSyisZG0IoP{0BTs2 zgh?9OdvQ>{e&tlthr?Rxbzjcp^B?Ah>X1`3^&8b>-(38@tzzD4<+&9t2^j;t0=ZEG zxgAg!VeVx)j4!*qzq8X3zSq@eXG4Ksyzg}haRd$>US+}$S$Q3$tZ=ho;}%IY*S8fc ze~KFV)lxPano}REyeC?Wh_Jf%cGh{ER}=X9Fg4VqA#%9?&YbOa%Fw7#lbo8}JA1B; zF-{SJvet{#1N*fT_l*GZ5xra5k53_c;EL@!BcJF|LyruzK_KI!s09@?swHJo$_A8T z$q3y@vhUx@tp|={@4Gx(FB0v1!sN@kQ*aDIQv`CQ1O=c_IP~jm%7=HNlO@WlzhYYK zcAWDzmtt-Z+MRqgdg&yc(QUgwB4$W40|YN}(&#neP7=Ih^Y)!7t!dx5;JmW3-9ovY z>N1`+cjCtfW_f~NH%gsP8EA*kN(paS3HErQQ?{xkb%-Gl29>Qn>!3}w9cZC<5$v6n z8v5auZ1YiB4Gz6U;U|g<`Gf>Vto7L1n{K-r7KG$={jj#}N)^vjwa>j5YV~~MMW*Po z;`J48L%AJ=2i8*ko12cR$5$(UH*)T#zZ1eAyY#M1KC@e=<%nzarFWS;-Y&bfHIk~D z*nZ=k%G%;OB`WA9U@V0iv+zzP%Ib_3{bft>WRZjvS|}=rUYb>}D>>#_vVdM@h^<%B z7vp2=9|lsB-=c4M8Yic3Z3R zs|!eV>9!okTZ(B}Br=@@NSO7}rn_dY*BuG4_VYCKf@_7L)|lN-puv6x;#y4NaZ}RO zkI9Ck`!Dh?^=THD@kLITtVEY$*o15aem-S|GhgzXGdsen*U>A&|EC9=`6YtgM$qEs z^>;IaT0&b_w3~xBb7u%yJ=#h%R-zDpz>7;AU zxjAAPuZ5rD?V&kk9FO*xh?Z=BQWLWnstoBW5D++P@|7BQmHN?iYSXXcGche?jp8eZ z|4jJ!)K%U#700_d=0{O|)57hV*%YIRku=bSuMWAsYNfzkQ@>MykQY^d^7%~ALd8k# zE z0{z!Bo%e)jb7j@CCnf?Q9uki>+km59_o+@Mxk)5+)7N1KAPPbbtO9m>n z&~CPc!zLZ!RW4Yo3br|VtFH=z+?d*h!A*l+B$su6aoXj0d-uY*p-o#N2bIC~Cz&?A zRUgi6eSixehmhR8eJDJF^x-^i=d2Jv8-U}YEB=U>^g*bjbwj_pGPeos=j_-Y6?N=G z$zg$;K?SP_*v$@t58}cp;fmJYULYjn1q|a6-$9d4Twe!GVhH2*$?m1}tTfoA*` z$8`jk96juSLF_GDYInn!+O+f9*IW$-*SXWf)bO@wIHz4sbn^OHI5bXd8FHk#XXnM! z5ejihI%pRB0D7)t$Qxz#R6VxQ?x37&reSPbuDf1q$G95^Ml3M$6 z65v<$nSAh?t9PO$Rv0R8vKNX;GeG<|-TT`79LSHgm!<66V$2KemP=7*sJW<5?5r2$ z1YfI=iLno1rPp?vS=Yx;6*nHQwTDn1+gkV|p?Wd+(ZOT)6(IFp;i!Q%A2=$l)lsdv z-qx#_hJ2YVvYwDkSc8WnF~~Z?Laa;HSa+OF-lq%=={d|rQ3I+cYoU4JCuKw+t_nBp z**jn8VYrhp6zfNO?^lvOjmy^{)2p^4tQ%W>0~R}>j(@^m8^^A!aZ}Id$WpN7ywkZe z$Ni|a_=t5OII@8gC(G5x^2sw_8fhTv_IJ6@EBcG`OTl@iuP#NEbLMv2fuPV~Wv#2f zTm4CK&Y<@+dC2hl>#YO-nlW(IQO#mZjqO42#ILFkw^ft1M6#6gBTqNtk6*kBJsj;i z5}V_e^4k-|1jk$gMVBX2kIE)JA&j#%R&!n|cCCFVqJ{7SB`1dDhPW|=(W*6*j%bYn zqe8uI;m+y2v3jQI@nwM(r}ba@wR#XniI}ZFB8DrrOiV#+D?;kEez_;zIpFF}ZB?6X z^xH}PmDi>g+i=4qMW4^I*3DW|U4B<}XdHfT7Fz6!BRyr;>{r`q%k))-neS_=c# zn8WRt(f0!!n#>4Aarc+WzC8l6KL+RrqgsD&;@Xb2Hjnp9^7GHBM27ovVj~NRqI~j@ z?`YKuLw7=dTvJoBZB=o>yIgtI_5BHAtw#D95; zGxy`=itj*2H=a5xk=aS##^%O3#24s1<;311)9wX8@`wvAt@cXxbW~I4&P%vHs7aD2 z;P!roI;ICfHHGS>4JJZ)aV(0|>@d{EMt}`%TnaU2aYaW_e@>4hwrc1#Zlv&mFe$c} zzv&_B->cNHN8_uTmsKo7(^p=OO)6$ zA`95k_H{`8rd_|AOp)_Tpe26UL?+fsv_XU**N@NIVz-&r z=s-noD|huV9;CvI>rbIH>bg`2A!lDR5uW?kle;n0TMf~}jmS2-r zPyE#sA;J05C!)T0@hlsi!FY)Pz0*Y30SOyJb`wE_>6P8oTv3kq=ks2)?{H@$pq9q? z+*xaY{4dP{%Ot&_)3OF9BE|bO`u*hOx*7Iy>b=6bkuZI$-)U<~xYAc8c1GWXmuT0+ zPyT2WshmH-<_}}GcW*Yb-uCWf`uqm~3)Wwj_spFP3&uFJkPTQG$JR)zw{&gd%t1%IdA`X#vV#{!WeBC&*Ll1vrNRi+JBouZ>yLEvUSFAB)#Ar?p zjek}ev!p=a(Ic6DaxDH}8fNqK88rk7F}${Jyt9%oL2M)s9BtnNc~HN2 zvuCc#aOM-}fRhaa9D_U!AujfpC0FgHnu6HiZ)SAuw75uc^XpuDCFXk>>S3>oW&hD_ zPt}CZTE=(;uf5ZFvD?NNIBi>-nYoAz2gF5W)_ytzlCVU;io&@eM0rrgy8UU)rIPH^ zLgF<{?X}aSfiaOF|L=`JY)o(@d|kR$7rkTKR%ty9T*cVh^rx|mgDZ=8_Jnm%>TMHj zq{&UT1$tnGW5SBP#H(dbv_f1-sFsmwUVtl(c{s_a6Zo>Cx3X{hXGksW(`*Sdv>; zbZ&5dMcn(5uGj&Rr zw5q<(U%LROhPcfE?Ubu-PfVf3{B#m-tnpO}HC(e)KJJz&?WGwu$wtTnLbPq8Kql6& zwRKv#MbFV8MlhxnC+>dow3Ui|RNtWph)cdrQ?!}kJnrONGJ^U940YA_MrjU61k$b~ z6)kSfMpSH@`jww2O}Q(g4_%7g{UO+dfE|#=#sGGt=-et4@(v)eylEWQH%pYq0g#WLYBz;L6dbzP9aI7!sni8FvaI{^;(8vvMi3V%*T z4UQoveIYZDR-2AFy5J0qpP|vr<{UfeMq!NbKPL$VvbKdHhze_qKp!ijrG%A!WEW+w zO!?A}+EriXuc=c59DlIy7uasU1pMBv(`sv5&$=(U)FHl#w{(_;*<7YA(Es}y(X`cb z*{W4SXC-4CN3sjIjdgEwcXx;($9JEJ9)nO48$P5~D9D2dKMNp&dzm|H4&2@L(t2Np z&oo{*{-cEGax9pv{XZsK(c^G;I9{RWaIcp;R)teAHEuwCxdVddkVc!y7o-`PpF>=R z6&sxDXHy7a5p+bCmLRr3fjvQa$}pSP%F^1lrGxWffs5Z5+kWk|-lAV=^X;l9xuTDU zmKKRpeJu|mj{=O{?_V`{z1j;T9^Ce7F4)K2p?sjUBr8ngJlGC#Lp^L`_f)<_=x%=H z=M=~3aE9tnsv-6{k*PD?JX>NqEF07ieC%|tPQ0mJ_gRoFiNi7T>lO1@7NmZ%fI*=F zGSYUUkvC;-@UUjsL7Mo&g+Y$`gia99(A@)%I0#2$sZ5Mdlcfuk3V&JTE9d-{X+$9; z4h}VHZAF6wl#Ry1!{_HRHN@Or`4PM1Ko%#u;68a+4vPph1^A!$py|y{c`8c$kt!;l z6t%ffkS8@h=W4XL!k##8ASJ$pe}Y=ST6zeELLi|4jD5c*{-&zcrZp#+hQ zN=6!;Zip?u>5w2{3<#mXNHcU|r0ZN;Bjf;T6BWQ;26n2bL74KDg~2|q8dh7?d(NM0j@E^@y%?ze zq&cTY5QqQOW$8BojP1O_t(0|3M7-ML{K_;lYlzQ$-*+rs;OK zSadeFC`gIXnH&d&-K7a$7z)D(QODW$3r#(W1C;8-9}z*O#pu{iV}rs)Eof$b02+!9 zArjYN)3T_?w!$eN#{2K+k2kCa43zdzTW6o40dT&hT_0S2-u5Gih91<=XSeD{8?wj~ zd7myjea0C%k*jLl*Rr?-Kl{8j1de3Srx%bfkOz5p$mj*zxzDc$otA9{HkN85&7J7` zdAYcf(7}p%ZrFdO^PfBjh0U<`t)sWiW4u& zQj4ssJT$!H+=Ig&iBm>E)Pb7yNQjylhC=n0eWVIwh)vTMKmdK$?hb~cQQhS0~>xML7p!3h5BKNc7OiSV|CRQpDC+7|$ z;wHK-0G{|99E~5286H_g*M$*#vc_hJpUm$o_Pt}FVWo+ugix#250uzffL@o!@RfZX zk9cSAG+Q|O%0mN6NaCl4p4@V2a%y~*zkWVFBWJC$}aCqSRwti=1ojSn0dB597$?BfvYZ!h{YqQ&JUS)TN zY}l5;OnMv?c&<%3nzoL6J7D+S1~RRq@oFI1)BEH3tL6zMk7S{`GsJ1A@m>u6J|GD% zAg;KiqPA?1>2n>5U=5#U`8w`hR3=4h3MRAJc z7tbJv_Sqii_Y>CuUtr#hOD&p;60sA%>E>vfM|Z*{0|4Mi(W}oc6#zuz=c5@b-Herm zQ`sVKFnUj-{@K&lrfBH&`KIk;rhQwiXM&%>lQ-sm>Qp+WzErrz3Z=5&_ky2z1#y{z z1Y}O%ISg3KxXok5n{9q&nkhy-BqQ<#7IP|&>vEZtoF)MBN=N5PC?OQ4Yg_T`InmgKuUlk&(Ck5>lBOZl$*0s z=e`?nk_2XGdNGaa6y{U_QuHBWQ=n+pUe3k<6XU(S=wc9hsz*cjYa}!u&IB2PBJD2_ zXHj5E0G~(EI>x0xy}RWD8rA5l&ZNkZH^LPNq6B;1}h=ElAyFg;462 zZtS+1_RlKkuKz@GfrLEfO?#2W9(7u=C*!7aAOJqt5z)KV&bOm5UtfEXBSou2SBsd zaH#IRueNvJ@);C_JY#s0ig|dpti)&LrLgBmQE8t*b^8zn1kB|%2``&V6aUXVuB|ku zssG$!Ss*Z=Xo=}d>*~59Z0z^UXrYIwa4hzuV20hV)wB+~|9S0rdRT&>;42RB)hhr% zFGj2lWXQ_p+kTHp#2uWQ^Lcqhnk|SPcZHg6{Bs7|HbcaPvR9x-$4;b^33UYQ`&XH? z5_S(GkKBej~ zXTckbO25noeCfFnaHWW&L??2)EECRnH&s{9@t^aj`YX4CZTuacELy&oJA>h$kkAQ0 zVh&Td76NW15?j+tNb;P=vj8y}q$XxcMY&9p92fplEGUATF@sNnmer zCGQ$zIyp!YS=|{g-AN$}Y6u-$V(jd)3C8&5sk?r?B!nJ6{jFDjbHR(kZLKF=@k2CvN-484HvVoYo||a)Z_7CS&#ES)sTXQh%ZdulKY*ZGASx?L=hnk3(hO-`9465% z#;b(zDmv33A|B3JH{k)QFcf;>KCSjw)RjMiWk0^T9m3fT$Yq_f)@!Ja3l1%v zjZ1;lZ;OAQ2GsS1((~2@{w=2cSOOdaC=61l#cP*sLCfIQAu0%!cD1x!zBBU0CCbnQ zF!WGc?W}>ww=u<{wZq%oiKBOy`KZ$<9ElRtD14wjLC|gcdV=j(r{>95aR|)OH>($e zp}K2@5UST%ctv(=(xKA(?t?!fy{AGcHH-hME5kRVQrJ&^<&PDxB{2SjHk|bc==&%q ztVLfMziINm^Mu3$LwiQGBWVHIzK2gk7edk0{02q> z1~RJ;W3QE+8y}4SACFwQl8V<7k{xihV^lko_Vw@B_RneO?dLv>f5p1C++q9qO!ft3 zFdG5}n7B#1oZQ3@*>wG=d6u`06Fo~ z{}CUa&N&yMFdJ_#YwkR=1nh#|@ZkHEf8t9u;Noe_OZQtPv}Q%tW-iNxfabfeOwa}M z>dAvQ*a2`>0D{-Y2Zu(`&D|rZtsnlQit-QW6}?*}bhpS}q=m%~&30Lsuwl-;`_$0H z<3#Edw65&mZ@d&exMR49)IYp@;9E2^)D9TWBF@{-E^%A+irV&TX6lK1M z#6ZKXYJV{xKbxAT;WQC>QExE0Xq{9)agX8}?wkVfrT-_$|HB%v1z62(^79c+gy3Hc z9fAV7Ni1i_v8hurH^`yeRS%r=DL#pkTmFaLjMq$@6Xf505|+LX+g(Pks5zsgsE^tU zsZ-={vWaG)|AoEnn=9HA`)L`QKAsKKtVhh>M~~#5!Jr!tiC`9Z2h1JFk90e z&ah7+!J$D=!&Y}Eft^}%zCx!Ro}b8~$a*0ZdgFvWx7+T)qqaYgjCc;faz`%n@qds< z&QZ;6DvM|0vh)<3L(rRrj^d2%EZx6D{wwwU;~V00&pDknb%c{&iW+*(2*v4Id%;oM z947G*YO*?}{~pC`em&I$2U5K6UFXZS?jbA0W?EC5_LDzSKYOZbvf(G^3pD31)Lj{Qbmbt`-@Ow`zc%D ztu|Sc6(&qL|I_xKR!$_1klnXJV%rznWTA(k0l@SH{Y3-S zP22MPNlWK^P!rqDybeWzG?KzV{~^fJMWkf(D>T#rdWh;Eb|nK(&KurYH`|I1yQ<*sbGyfxG@l)LVnFmxnhTL8kq~=k2En#6J8=Y}*8ONpW}{tNC~| z=x73Mc}}f)(oRd;H(=7e6jLq0P9p2f_x44JPU|6}<=S%&WM(o%X*xtcZmZg5!TuT@ z%5jthX_CBLrrBPkr_@!fS~n#9K8FgBM0<3%(J{DPSRDcmaeYZKii~NOX2kjJV!6O+ z_W7}zT;`g@;(dzL3eX6^S5bE<0LOTOH1>v&1Fp#$O^WgG$~gTu-=K;AcfP@W&p801 zd2OO62bziGP!M#Z<6DIgpUi*|+k=`!sYAt#%UN8Vm-pBJCFlA$;lqv{Zj5S77)?e) zlr9pk2S-`M)wXL4j+a@K?S%Q;1yGb341l_y(d0{*G>WWW{B7*qJAytA6hln|4u3Hf zeF@wTa7=tcgEdzFr!@kcTLCsv^FaTMi}k(IPQ?D^ZT6eRccW_m$(;Vbljs5AQrc%w zH?CEj;U#!Ck52ENI^A3Cps=4lGMoQL`e@p!aE&@`op!I2mB)1MTS=0{+oCNtB~W~( zC;@EhU*;)GZ7cki8@0NpKIA>HLcBr0JuWBoYlh%`T3hF9Q^Y2|Ux{XiE*XJ1vX|D` zw)HH~`6*xMF^t|^NZxuKtEQGGGI9`t-rWFAGat-vj&+j%#6&08xNkG`P2B5G@~@N- zZuJ3~<-aU};d4UJEksz&3>teJSP$YV97+rJU5T%_RwH?NK2Wazlx?Esd$Gn{3gP&jKb(lu-(R^u>E1rs_ZMX;M%Do|&aeR%;?9fXj7sEah8RXxC%7q#%n{k@o4U2;y7a&4>2M$f zj8VN7h#I*$L?^wto!|!s`2M#6olV__uFsmTuAud8gj`E9-et;v!)Keh&i&c`e(@%I z-g&`(Y|gp0kCrQ!v+8Ws`XV?cWTi6weii5^Zu%;=#1v}HkqnNqPEPG?JWmPEe`!&S z6`8}9gAx&Tlx6$Gny79;H*Sy?)Xg9D0*&wjjhJ(ogoFC-ndiW{*JbUoZ;;vTDgHoV zrKNUVyIpP+`^j_WTbsAzbxgu{mbxTewa_WKeHnePc+S32yJnC379cb|5^v}1{{(c} zRQD%`TNt%nTqE8%OGvQX_uZ_!t|22pWS|2T$-`z=<}>#pZmjBmg|PGbzb^ zbj_|XD&Fke$A((x0efYI)%M6~|GNV$Zwr4$uum1(T?h|dN+lTzy)ThUrl%R2xrH;B z70^oy36j)|`2K$1As_Zh&MSOBMm}N_9;#L#xY!Iq_d!`CpvhD1n}tejdtw7>Z7;2( zm^wB);kIS=TR+gqaw+efhUM#mh#man*l#T}q_=e&?E{6iJpCbY>e9pR&B0A0g(c@7 zsx=$13wYKD@qRRjEUvgbyNi|Y>Q0(K)_D@i+FAwN=B9=X`qkn=@S(cU@V zUX{XeDWVmPwc1W*6CA5y${Vj?${ka89dI;!cST1b<0mJKI7IColJi|GystpkQKj#a z_nk{@2fhzQMCf*<#=5+tm5CE2;*yFRuuE;^E5HlBfCC*;>$vqTMrS?W7{Ia-#2bB+ zT__QUlJip7*Fcx{J*OAvmI+chw~uHx?7o?5?=%#CruaK4@dU@^%Cr5OcB#e0M0dpSfM5hr+}mfsM6Ozq+}fBPThC4oHn0jd-FR-N{1l9dCYvmn6)DQyAT zy+JA%%->6w-_t&+#Z~(^kS&%LC^HT z@za_xW9N4WSdxWL`)47ilN3kM#0^1kyT=AMYj*}7nQXA^U1TEb$P496toooZ}*nZRA?cB&|q$@fq2h z#(hcOpTijPLvU}~Tq;b6hfZOO)@5I}Z(nzEyi`3DA68zaxfo(YXQpJW(tEW1*HN5# zv5BT+&L@q`Zmp)Vgxm3W(CinQ6P`TH&yIONYH|^YauzoqTZ}zaJ&lfMHb1@3uEEWK zp>r!f#>ZSyds(e0d^8h(i(Y&t^o&)`Dal&N{=(jJC1b}s&78W8<7P(2&z7dPjSG&*tTpWHLcdbl9Sj}PbE)6*?wBhi*>-9yI2W>904{Gpbsn~=?e(OcZ`UN5etTTNzHP!oy@WD`VGA+VdZbEIZA^~dKjS0>&o#@3QqE6kx`Vx<~Z07nx zxmVc?-swjgmgz=b!wLwv7rtn&5%fMVc$tg3$l3E-QO^6Zl&6h%w-uw7RLQ`$#&q+l z#=>7eR_YU;&c=wAEA3QOIQqJmNvsWC1^ocZe?z;(Oid{mQOYi*dW34{auNuiO%* zLwuedNLw(-6&!oAR?z&Wz42vsNj=S&$d7D15ZQGc5z18E6+hP0k z5mwhAC5{tdno^XphIr+`KALdTmz)M?{9<=w@5*JOW&Zl2B+(vqi{?qGo+k2)j-aAy zq41P7W;e-QOt#j@zRX_Pd@N-AaixWIdG+Xk2-lw?=b9a>D_Sk514Ck^6Ms!9wBlwD zN|2L<7Ud1FI{_1-pr40BV&yfx)6W2}%KMf<-}9{oVR8*vRdCxDI+DC~YNU$LfgoT+1ziz&6`{2?r z6CCE9EcN!?O^$0B3TGdu2i^N3NLrcxoU6vOi77I~6BP%MGs_MrN zeBnI=OA0+;Se>DrVm%u!wU!W8^Oy@W459(t)H5)7X9k;x0rxkc&w-NDc?Lo%_?V

    ^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 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 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 479/734] 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 480/734] 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 481/734] 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 482/734] 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 483/734] 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 484/734] 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 485/734] 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 486/734] 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 487/734] 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 488/734] 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 489/734] 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 490/734] 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 491/734] 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 492/734] 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 493/734] 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 494/734] 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 495/734] 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 496/734] 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 497/734] 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 498/734] 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 499/734] 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 500/734] 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 501/734] 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 502/734] 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 503/734] 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 504/734] 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 505/734] 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 506/734] 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 507/734] 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 508/734] 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 509/734] 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 510/734] 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 511/734] 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 512/734] 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 513/734] 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 514/734] 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 515/734] 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 516/734] 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 517/734] 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 518/734] 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 519/734] 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 520/734] 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 521/734] 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 522/734] 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 523/734] 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 524/734] 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 525/734] 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 526/734] 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 527/734] 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 528/734] 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 529/734] 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 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 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 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 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 z%X;latk8zMwmHnMv;@c~VuNJ@FnGH;*>NRA!$aV zp6NOJ?yBlKGV|>pRay1&WoC6%byauu(BFvaeEIU1FEii!2Fd_P$^kc2kbQ<+uPX9T( zmPRab!aCMb7rI!g_m(z-D5;@t*J3&9*`oG4L9-GPc3Cu*LKG*746R&`p>*1)?OHAS zXv8LL7fsir*#}q+8(|$?j5YrTT;OPAd{&iaoq<*#OL;wAO{2Tk_CAIueOKaNqy{Teoh-vMl=g`hph~0x4x^+baAvIzhA4Io*tWJ`ce8^XFMu zSm3~c11n|&HI0#x5pKQpRx+6kZ++`q4cB!I%d!wc_}eONn;EECmDf~R2+`5@{j!wO zsPs`=%3c|Go`>T&UOt~El}b4eKm0J?`qsCYn3#y`&!$k1f$Din0o;E3?Oc8J)yA$} zy9~>+EW2b}8|!Ewi%jEx8I3A0gzHUQp$PPbx#$G#V2NFX1+N&Ir-H1`1r|OE|e8F{~rz@*GX`V)JVdarUU(h#`gjysIkyyi8=(9qDpD_{A_w`|z3;eF|J z`t7!DUu_u1s1PC*ckVhm4-Ml!%X_uRw&{rk;yW z+lPmTKWG@nXwy%$j@5uL3}aw$aPWp?GC6Vn{P~H2fq}x!H{a}j^PAs<`T`}iuTEj8 zc`zzMb$8u$7n?V4ws!2;u_v8Qzr`?&k=CAN9jgV)vbJv8wCUfCj*jjYLZk)<2aR33 zc0rQ?QZ-j6F6tbp+p%K@7hQCbkw_#`LqkI^OC%C6Q;+-Zy1wls&02;KVzquoUtix1 z!^6YZxUSo4S(cSdCZRqLgmSwYnam8-9XN1+;o)IRN;xn%ICxvdP_9Dp00P@i(yV1j zDOcqOVB7ZC$jHb(A;g$%+lhPbxu;QYJTfB@8jwK6Vv%$@Z6y+kfn+jyrB1w(F8WH) zTnJLG9@6Rb<-m}XGST1PpQFjVpr$=0*L4jkrIAjjhfLEP(~qx)1nMNsg&^hXA(>2W z1O}v(3EQ@tjt4f)KqDg~2q6;5WO6_V(W@Wt7Rk{rnhRY5S(cTyZ982ml`O+Bnw~9a znt>e0!8A>PL}i{RfNqf-?V`ERB@kc;AyQIGt5hmAe{yBpup4jN_67e|{+hv{EJ%C- z)Y7t^dZH^dk5#$Q`sC;ZgeAr`){R$HSTpOCE;#1NyONobWX|!JcRY$-ZGun$mH}x~ zFk~AH*#>Fz!ji&52&1B|>*ceD_8VDP7=>6%y<3dmdNhtFIh}X;$*jY(8HZnGT%ON5 z)sx%9ZZVrIG;0F|bYM41mnt&>XYwA8PC7hvw!mXE4$mw&EV?qB zYalB-LMmmzzn&14dQUl4Ya{PTj$~bqWV3wrbdF2>i@ue>Oi}Wc7xMhyQ$-HXJDe?4(wbC;@yD~FvMe)^f3mC`P?@iS zyeoNfp~RD!690H^kxTn5-Z<97JGP}6vyE;}j%sN!!?JRBx=8|+Jjt{3E)R?s_`2#*1ePE}tA<;Omo% z+_j~T-`>#6OM0wsl1MEXY^@s6U9)!U7t;m`DQ8b^s)rbEK2_O3q?No%SEz|`ql^Z7PZk+205gv{5pMKrO}Ni9V*jRXgq}pt4uA) zO36P@En*7z`@LhB-DhAo9F11JFrB;VNurMVr%?%Hdim#5E}wiV&p(|gl65uaQ6&dj zT|iP)8>A+EL{J5VR9K9s7GM(X<%%=R5&X++fvJ+m#)R2W-*gyJ-nNe=1Bs2S>8_qA zgy?u6G4kRPzJJ!??niSxGv`wBC=YsdP$4*4l@SZrD3u+R;Z(-Z)mhcfQ6*WWeP5kP zzFG_zW8jBkNq;w`Mj(R zG!S5#K(!yg(c(IhB0-Zbej^b=r3Rn5geGYerc-%KYwz-2o2|*NkUlL#&xQ6?nEr_s z)Uuz(5j88XtDshCR4h7@Pd!=SW4|bHeo<3F)v{JEMQtGcL6jomJpY)i9ru|?r4O}N zkv0K(za6#Rz>`<^M=g`PxA%9GJKt0$qK`NgR(DCWl(J)iL|>)(<-7}j@l=sdJy9U* zRH+o{LRd9b)sK2y@tz{>;wbN|nX+m?OX>w1)0W_!+td8^hMrL0tRfH2hl^pF35~|W@wg{j6Wr0sWSu8V;pu#wj7A`^SS+hAp1F6|ZArC9gV~g9@5W04m zK5ZEs*ge1>?dT)bJ<>-nF;oDnVt2 zCDoaJ9zk`bAD1%vyk^V3FflW$-vUt#%FV)L>a?^}a_|M%pxZ$BsVUb5S~A z_tXnltW>#zC}&D{?RB%J`q`Cwk?5uB+X~l{{P?WL-QUkMvnVwU5inW^1_z^{yHMKs z^@5E;)V%e8`ioJQk?og}HU%HPq@Q>0?CU0@xgOSTAn`;-^6{S*IW`+%txCdin$R?b zUoV4W8D}h4&Py7SZM_CtdJMMp8l-JO!YDs<(Srro<9xy6Ou^&iqD#(C3kjKG!EnG( zLhz2Ay}WB@nnZV}2VEF}#4jFk`1T1G*Mr);mf~SSjgg|_I-x=J4Oxk4?ee zw%FXf$!6FV^jCff0}KQb@T8_H>TVPGmS%C^2XnynZyrfr|&ZDAhf)___^LnI zs}iW*6hyNUXnfw|V~>|8`tG_aWqe5_AvoKvNE}^-_{K}6oH7J2AG5gYk_4~YYO}r9 zV7caqAz&;a`2CA|__teVc;C;9YvhEI=YYB}3} ze@Maqo6d8~RG$B|xwq4^_I^9IvzrZz@&7uM^$lP-auti5?2R3prxy2{preoOGaYm`MAvvLaX43Sx$l{IKJ@GYi;h&D zi`uh#P@Uzk{KgkuzIrBy*FY*v3y70Iq5Vdq&`}pyHcn?H|9sRVAbA355`MiZeIRHY z)Fe;RgtzQT@ZM_`f4hsZ=t}d38ZgB z5-3v7>GezH@2E8O@_`;b<#A-P^14-Rn>brYwR_Z`e~#dWjTV1=U4o6NZkIdd#fYCe zn&opZX33P4Foi04)H2G8MY;ZPro>}&#h5E}1R(*VOCX&vk_+nv5`8RXL@yL2-+sa4 zTvk&{fsRyvQt3>TPA!)=_6q*;RY~>?8eMHL`}pqpBKIH9vFM;U2kKTT41}iI%D=Nk zmv2w!S28UAxVt}0=i1X@xOEhT|Y%u60W) z*Or0=o|tnuSMb8mrDqjXRhaVN)hfZExe_M})lWgJIHD%a)cQCHgu0EwQWEItNy#%) z$}u&TBbCaK3$K+C+_=f)j!Ud+)VCXQZ0ePzgp!mM&y%2iQ-vaFtCCq!XO%uUk}dIc zrui?6HGqcmf#Ob98;MGJU6urT=$OZ1aS6R2$WbP^UpYRO6#U^;HZM`!^==3Z)+-wV zuI{(lkTiqO;{`p+==fJqwPhX2x2LKR*mk0!e4tecBo{fl zp5xS6kpA_%Og8teG4262r%ZmcGH<3k1Z4>ASE-9|=U>PR^&uDr1~ZZ_G}yY{4yk4;IFmk!zl<6lImY^e2zDv_w3@NCYZjp*(` z;;$xs{jJ(N5M&Z+WnG+YQhw7^OzmsWmOOgAjUf#zsk0if#R_Gfy7Kv z^5QI-aTpgogh0nCZfvgw`?ju)xKBO2YAjLy{GDn{$Mr?vr2^F>JD&5(DIlH2>IV{M z7a{M+u+I0P&xM6LuYB&s0|sMVwXm}_j3o>{iZ}t&o+C&NyD}p$#k&v;it}f6~FR8v} zB+6i2sN;xWurf>mBJSXtf-FCQ-XE%XiG0k6{%a4S_#u`TT2%s7NmWmcML{Tk!(n>` z0Z&Assg$K^V6I_!lJY`I-Az|R7=b{mPosHftWIzEwoa6>92L$6>Pw!bU>jfxNp}i^kcy`k#Q7X7Ixy^0HxV6gg{D*O=UiF-Y+vlNEBip? zcN`JeLeSHme?ir>3Ts6IwQOvsZ7Lrc5D}6_71?M~N%2Z#Iiqnd-&VJERWM(aoLa1O zXFxrnNn~I8`2FNFQdycOgdlAO$GMSsT?88Tz#tqbjH(YES8uG5w@$@y5A;|lDoiU^ zFG6%(#n?PHBbi)m>e5|_iA9goi&|={o2l_%Pb!b1qjvkYd_I!Y- z^AbpHpjin7uy>QHt}2Lp+Z38m=fb~?dw6S@hf1F02j@!EJglgfN3nSV>P)4VT-D#U zPrfuQfm()|9@z}8+$6#b5pq*CngGfjcW6Q~9ihfL?UA($M;AN}&-(9TgbG6`>$ag5 zM7*khb^d8fs|-|CVXa7@8+Smjt*b2=A)d8;ErW3?3lILv3-MQ{J+gM75&<^%!|o9XyhTx&h(6mzvpBAhA_9J}?*73SPShdaMX(Q}s9^ zyfZ)-;pZ~&^!diZ3EdJ%d}7+=t4B-ZotPIRBxz*C@$$JzLvZWH#FE!-0^J=*^l{ZD zxNL(G%kU=?19`zq2ODie&^`b3Z)rFl{LQmPo}E>fBUiXW_e298neL62=`T&2Ts;`w z0T*N-(Z`MQ*Bh7-Pta|F#F7X9{qX|dop36uD=HEn5j+g+E89w6+_ojb_MX*M z->VWx^kEu;-@6`m3=)3hVuEW!DZ!`TVDSF^27@tQBI}kwqATI)8IOPeP>#PpRw^$h z@Rw-E1rI}g5KX^1W$*_tNztARj@Gd%fkYLp^8-rBnH|7e)n3qZmZvc znju+<361m!?tPWPyKXcvBL2lwHw6+)9(?Dd!`mNT`Zhjg>`KN5>;Hb0p76>hEu8=OSMx+df|V)NpR1R9lrsX(H-tVdtA z5!*;R0*NXtQ}CuMB|kU@fBRGAO%;Nz1Mt3A!+*S11k_fe%v*BdyDxY=aaNLd;Hr&+ z8@CCz_KUW8ZzWf9WJdD!;|^bXy2R0W4_VeVRMT~lR2>5W!DTv19T5DtQ7VVQD@JVI zygh+kS=Li87N@R?Y2szqm++`{p*4f8=0t~XaJksr4u41L*|x*@A>wUVA1fube12lAI&R6eXv!lJAbYJba?eG}RTw0!8XT zbz(S>2#>-%Q0`u#)v26RuCxuo+b>JE5Sz|@%ZvDB#R{!DWJanl1u@<_oC!`FM3!OZ0>{2X~Bj*NGAjdQ{n;Hl4Pz3 zlZ%qGImvWh;-N?kQI-&>jO0s{sx*;`s!|vW{k+Q74>ndL&#N|C{Flo+`UUhCE>N>OgkG+kAgD1+z>W#p zZ?yv`_Dqx=q^LP{zjC`f(+2ll)5D&D?)x56s|=(X)IfJI(&+{G&_Rzc{Y+AD3C3D< z#tF>I23~y)I(^jn^`KeGs;vX%rKy1>-N9QKP6|G=uZP!e?Rd&TOO{|;0*R~1JpQ#a z1^DnmkFWe(Qg9%k8X#!M)=Z1=ezmN`p(1HS482~K!8W3%C)9f7zJ?Nl_v}sb8{1bT zU9~08dRc+QZh#Pi`8@o|!yaGyi6mE$F$_U4rlO~dh9+@+F`$=~i|g%HnLtrh)oZB? zCnDlNGCQ{*P7P=+)JkZ~XYSP(=F?|5Ch0tGV+5o2P0F1-3nw;Jw!*_~YwR^u=hdMqKnJ z-S~QA8gk$D19741DCyMpKw?Du4byz)hwyhlr~KcZr81DJ`iWF$LZMfrmfD7{e)P%C z>Ukna6HRhkH_Tp$rvmT`2|c47w3%=_G+*eQYN?-J^)TBoTq^_Uvt30$jQ z`}$x-9Ir5^DFnNR1fRJ%#cQ`)tCb+vQXnyEB2}+nG9z)NjB5QlLp{)IEA|Y8)r*fP zZwi8pp{uY^W}x4OYW!-~i&CMXa;^P@DR|uui_gENhg)~78uLJlfy8AS!>j4WZ|R5E zUJ`bHqgQrBxXUtzz6#$~igURYNffz~#6MSP=a$QsA-MY*o6o#D#Z6nfFG$!3)^s4z zGU3nORL*QX_#Dg@)Ysp)QwRs@>IM^+I<7NV6n)hBH4G+T|2B(zUtx3ewyt5EmV`;G z4Ai!a#N}eonB>!MhOa*jpL-OZJPodzm?hLfRlFjhcw*J@R_H^#AVba6Tzbt;s?w+- z127E1-Z8=3ud(>8-4@%@tEaCb(2{4}YCcda*Xjs_5NsQS-@gHFx)>gKLh_}@1t(^u z7S~avQlJ5}fmbTjm`}iy@;yA&!_vzr^su?l;P>{L{D;ddt{fFuqDHQxkvyJu(KLuq z9*~bUA4v3JnXq>=?AG7 z)EOZCd>vV%uiixU;#C#an}Ewk1TWuW@|s-+`?ncvO_%L7{XXNQgI?E38U<>Qu&Inh zB2HKnfkYMh+h!Vqi-zH%VZj?OhZkogPo5DRJSsSJLh|CQAXoBLk>3d6a@Dr+nMj1} zqLeMTcu25+r^#>Z7F;oEu&o~klHtM9@3WIZ!u3F+hE{d?P!A0C2(H}BE{BwoTrQV)T{jbZXUo8H24F3HD{-k~D?w8%7ITi{6on8@DwT>OYhOm9<2Xnu zom?)LaU5ql_Rf}pRs)Gq+vU=9y8TXO8#Y3d&1R>8Jm6FU#7%#pOx;G+2ae_Qd4v#- z>$;gtCi8r^7}3oH=$fJFbo;9rNR(1CJ3D(4$N)~MRDvc0iE5xwh02Kmt~F1R0ssI5 zc}YY;RP5{P^E}V~p}ZVHm|ysT383 zCZewOjKpjEX%5z@yoRX+JW^z zVi>OLdh_%1GlvcxdT@Gr`Vru;X_}K06BEU+eeG*ax7RcS0ZdFxuy5Z!1_uXSAw+3z zZf>qnC^&{;By8L6w=BzEDF&*S#?VO0L^Ej>YaN+PCUff4siQ}Z9C>7BX6B%j@@c~` zP8SM=+yf6hAYXXlg{IqVDI*ceIy95XdBv$;rv10|NtBjg5_6IW#o1 zxwp4>z_#s_5W-L=U84q0-F7P)xzBE*ky3hvLLr~YWaejPX3kAbO&yz`pMR!MC>#Nv zG7RGkkU4SUgm>`Z!8rQ|>SQE}IwviOJ$v@>{`bG%=s&~0bhko@F*&a5=70q$<+Kpuln~+!aE`L)RXl$DxO3lq_qBYjmNF83JpcUj zOioUEM~)nEuD|~JX(2=gI47k%3G@Of*L73GN4(e30Z1vm%8Tj&MIbMP$Vn;ZD`S+; zX0y)R+?;pcefPy?8!CvUS(XG+@rh4-g8u$~qqn!$ux;BCLKuK0rL=r8yDoiJ2I?fY zX_`)jp}bP5RLtk|-h&T5$dgY#>Am>ki_5;2KcdZMB1nk+0V zVB0o5Jw405;dLwx$8lI#SYTja0L!w-X0uF9P4R&be4wdp!$xS@CV@JH^C&-P$>gNVRrL dzIC*K{}0+eht(&$YL@^2002ovPDHLkV1l$fv6}z@ literal 6636 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 qM%IAf*eA5}h(mYB5E4T(^hh^D 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 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 zcmV+`2-Nq9P)=l46m^SlikwMi-i z4Q>3SGtfp*#tf-BLCww0yO7iVXdkpcx)-_wx>ck~`6qN8`Xjm)eII=neN#U-G&D5V zYUbxo5QJ>XU=Bcsqr=eNo`d@h{|C^=(WQ{}hbLY!_n>H|oJI7D(e^nF;#Y8|q6;DF zt2|-^pFt5Z6G--wJQ7a9oWp0+A?o9haf1;AF6g_uy%knp*tEyPdC!4R!^2$Bqd_3 zg?v;RITbw;{T`hG)^nyn_>1I6%Iuf(^H4BUA&8YMg097DvS|V-cF*#eE(5Kd3K7T| zOJHqit7QUt-C|7;fIV^g%yTq<;@?OD+Q1ejjO84HC*8pR;~XcA+YmPC+dk7kjr^ak`JTcWR+z*tU8BIs>C*vAs|XYAVpTC;`%ll-DeMB*+z1!EawIV_1Ft$w?iBv5Aa z5`c$T`qac6lJvmQgU3cHo@CYcAZ-(D8x7Prpc+lx2?RY)QbQ~gTg!M_>R;6aX|cD7 zCEz^(s+>@F0znUx)CkMO)-xWR`)!aA1kD3b#i+Uy2vYS>RYV&}B}g~iZEVo6T>?RB zj;cF>prs^rgk@su7*ESay?O?z2}!33Hu4&P>L9B-fgqRhtY$ntOM|H<=v6*Y|DcVi zWZ4c-?^Y9=pjoz%vn@92b+nZ?7|XyUf?RHD!s-L)D%(u*w1=RB!LKM?7{*(~SWZbI z=sG@VGz2O)3wj>z5WkfR1gQn^W(TaP5Tt6r1_`xUeH2jJr-eiA<}MJVqjHyy+R=>V z!6bqNC}0=q1aRyLsSB8o59FwtsH&oiRFrMv3T8SV+Q|X=Y6#L9e>^3rJ#&Ln7$~ zGXVmwV-oc?+sXL8Q~bfpR)7`YVLWSr&~+c>h)aG!J&>})#7g|Cj*N?elj@_`&(Qfhfy4wlo6@GuI^hO=?g(0SP>M> zA~jF7EW679Dvm~cGIQ~b3i5a18=}-Z8FCtv^2q8i(+y8$%L5|-RUFmx({gTPSZ~>iLS=bJVbqcwC;@224`2(E?DNr!N4e*CdSu z-=xm2`v7>7afm zy-TkJu9Ji!XfvX#P^Fhm>OsCBiD3|F!ZEKcgUY0Oi$^Uu zxpdg(Mo<}1s?{8gs^j!{)Tgi|Sbdj}N~`*M9G#@(+7pmFLDh4eOHkj`FbLJ_mAV{N zB~YV*RNZa5k7&~>)dp5=fl_T~mqUp6mjzVkl=lCc6J(QIJ=nGpl<`++BPfHQ{{RZYh!O?pV4@ zK>7CfeDloQ``6q#_uP~7z8^Hz6$tTZ@c{q;p^~Dk7H01F?}6Z8My*9hT+D>$qG;d_ z01$Qm_W)D4iRb_T7(hu@N(Y&7nE5`9UN`f)9j&9gg8r1=Z|%kbhNQBwtubyU<{(v| zKpTrKPfvu7sR2@_+xyZVJw-9svfosh*9;lhx490R^7J?6E9a$3RDmSGVa#|5{Z8!e z*;!dxZLc%W|Baa}O-1Dq3A3pZmn=GXTQ3|E_uZE#H(N7)g66$K+auD#_!`_iBE0F=V-oGG}Ime4W+yygPnFux3R6cB`5rg z%&Ian`G>w!woMa$H6@%qCA`>@A9wTKEcr%j3Y?!RAB$u*3u`MmWRCJth;!uTZxt;6 z_WH+*zjX}R`?10C`8cJNB+b(%>8uJA<(h@#@-Ft@<5(T6>DZlx`Rqw+?@f0GQeFfb zGHT}Y7ttQW5@I)D3HfzNA@HjNr-Ym(v8k3HOm@w-`HwWVTJ}8QL%a(*Q7l-?%PHG>m_(B)_26sSY1+?prD|vUZpW!e4h-q6cmq{g}J+sGs?l-Jf|i< zpB1K*qyPo$y`+74GljFB5#l~fE~nJJaXLFVsA?AwAR!G=;*3gy${}oH`V$pp8JU>+ z#IF~%Ys$BTWO6cJd0U#BqkP2}5eV{B&Zsue{pkvWYRjJYVvHkld7PEG$-FF~U_b*v zClo$(MA!NtkJQaLr$&nnQh!lq|A>o5qqASRtafaWVJ^VW(1bLe0k>z=$?w|#UTsVL zw|-uGd-m<$0AjS5%V`gl*>~&yq>%>W%KBG`Wdt*SujQJ{4LX9DDd6T;A}cN)bFM54 z$wmf;bK@4}9qTAY4+?j;wImS*-c?}k!2Uw1!K5{hlZ=&> zb;a+f5got_`AJ?VNdG~W^j+)arz_b;`?v2R5xTApO%MO>#ugS-DCAB(y}kL`)}y%F zw$J}a6fhlrn_nn&@P__+$p`tlPQm@!F9o_DxSjrnkpgTHqeXkPyQ>5%FC$S5WLA}w ztN$8}7YbE|xZ(wmHe$3ja_84ZoJ&+@vr$(2?&9C!0Uq=EcbkEuWy~NHQ{9uSHFCB! z(1THgp%=5Nti;hy)$OMOTt+17)=vVzquyrV6E`TRBf^tIOz1MsP&*VlQ>I%kAtWZ| zXYT9!@+b92Dn8S%!|J2cV^J+*IuP9+!-T8m$t^bofVWrw8uJxr*3cUWvuNV*Y45TR zMSxP$55J`*&tocZZoK=^{=q@PZgi>cH8?!Qpk8atGdw&T3IUfa99~rz)NHH9_qjLL zsg&2oG>zDv#ouugB~olPBbtGMA(~Ce5O1wu;}659w=ttp})-@Y}o8o@tIvY zi5#{&#`G#jt_G=4s00OPW8A@7fXi*Tv~Qz&Q#$Rohy_ebLxTuZ#P5)UkN|OmexJrE zlQGMZgFCU{)-hwUEI>ipp!)zx&qm|O9!v7wQVJrFXj$UqOL-bANJJMrp9BIQ9^#4Z z{a||$_nUMpLmNSkS-f9KffdSG!!p$PZ5yUZvy9kP#w9A_Y7Za5{{5D~R(~Cqz*=7L zh%r_HmOnTLe0s2Ih2Qs~BzR1uOJB89HOazsJ0Woa$pyS;kxEsp?pCXwd}^Mq0FSqR zp*xVMPQje;F94-nBtuuW1m*mV>N;0C$ozPE+Y%FEL4&((Lm&G@b22Lm0Q($O6_Dw& z^d7JZ0OBlyhVddr7Wgbp;6Fd90v*DcIQ~c=LSrRD z6TkL`4AH>Z8BTQa5qN9kuUe@WzxIjw`AB&zmgl{xl*K#hW>3#S74Rw!UsT=dP<0RTmb?tS1=zqqT z8M!*$3219+A~Q8?hv~|QLUdv!+j3Uj`Q%R~Ha>Xt2$w|ODpys`kc%XLkw`IbI7UKn zS(LJ%r%demG@auUt?4EGp2zVW#aaq{k|3{X(&QS}50_cI zufebX8{H4_VkBN0D1WI&)U>OEAU3D`t4rhBzSamYq^Kr=2r_QnoM=C4IBbNl=YMwE z{1(6MZ(FLNkDT9&!=U;%Ae zoTGV)o;TU?EKgXYInxP&q9Tb(GG48e-MNLlz;f}!zu7*9j6r$$CyA1>CpGy>OD9Eh z-~C;7o^!t(9eY`$%m5xQj#Uu-w^&Ha{!;S5v?7?J&+#!dkUGn z5x$~u5`hqH6)=xsaSE3KR2pw~cAwQQg;o7Ir^=+$(7>_2)Yv7=p%bG{-T zTP(ti2JMh&!(;X5xIqAOUf|-*9VAPlP(PHVB1Go+;CwYPHhPq`Zn!;d{FhWt1IX{T zlrz9vI$Hc$XV8nE%uwUGFBSK3-|e};w_j2>kQZvACZb4nlNZ?|A)oqkx!*4#t*tb; zE#qGw#=_ghd9Ccc>Gv3-Z1QEhTq0A`BS5<}HY+VPE3pd0W}%FwkE<2RujF!Mq~eU2 zqNOZ2gE6Z2>}3XkfQ!lSJg`m=RkZ8Q?`6j=Z|ko`3OSL3rz_bpYt|wr+)99B9PksFCjScC-=|) z3#lzk*=R`n{)fE=_;z|gNi0CClz_$_CBwojQf$;wCq;5>cGUaWe@pB;+qL>r><(@z zns$cxjnjUx*9bd*RlD{O?A`H!`$poB_$Mn3{WTHVkIc~ET2qo2`}$AXn2V@EG)s_= z-h3#^pgqs$0}d1TZFR4L2HzP^FB#hI+Hw@aO}jM|ETmCWz^Hw_#*KojE{^rOLZF}HGeBx9t+au?AvlVwgF%D+TY`?{sI8Iq2i+-1#55S}XVHo~ zQ{|Q`%&<4$^s7*jrGvH|HIi}ERZ-rGv=j)6h?6^+^dJRczwrKWb3P?;K<(0Hvu0v? zF3@#(S9E%pp5|*0L~W$}VUzGjQoip}S`3sYF6dn`m^Ou6#NQ<$*F!Daq+?y}JaK!_ zBj>C6ZyMH=6z+VNMmKUh-s2b}M^L9?0p;J`yxyKYJtNN|>{S~;=Z-zA^K5qAu*`mZ z_f`7vIlhNm)*2n(W6%6&O%RpznQ{d3kJU|ReC zh(9k0ezBgd_QoH>%nAZ>PiW8rh6;I`C^bQ%ORE0QsFcQU&~}bliHUJH#eh*De^lBxQo)vY<(#Y4h@^+>vu@F4cE`iX8-) z#0`oI7YX=RWZAJRcO%|hcrWO`FH7KpEmQTWa|?v-XbN=Z~^%z)kekSH_?~8_fuh|f!)); z-hXze&~^=H;2SpApqaHwmiTCXgRX`g8B8Hk{q`s4A%b)p$W2er*$K9Ftj1l6btv#; ztj4(E*pk@LgbXkIkZ;g@B!lEC?Xq%vfGO@NY*BWE*8Vdh$E@}7#!g13qU=~{x4rLf zzDz1H{!On9V?*ek0`fo%UyxG!+eR48fi8EuSGR%*6sDcPo)4eD=pvzM&-}0!errZ$ zcZkS|xZ*M=NHH4~Na}1-@dwAZOhl`fUd>J@fh}iL@`4z$t!^-x>Tt%pm2y472yoQ?Go+J}=@5tt6r=6Q6207Ju82|hnAm;1 zK5F%xKZe7RNgF8G&pE0=#6M*GpcV0afQjMuB%RmM?CG@QkG?_S^~=#}#ea(GrcnMz zY`5P>oCIBQ?i~g8Vn7DD4WRoA&n1sHU4=zvm4Jw85AFM@$Gnh&D~=2KwyY?DRirFe zuHTcsT!lWZhlKn%?II(xpykh7v#%;exv*4OC^ZR*{QpP`FkiE z_hnA*aM7vboe)YoB-z;|BRCyA7Zdspjd*eW=w{-1&tQNNLy&Ru>N;b8f$`VQ+>)QC zs?RLmgY!6-A+%*H3>J-&;kVMckNM3sQq_TeUNzuLlJt!8jODM4Z6sA1U$5+&jzL>r zd|b~*RO?Q^l%^H>@a9d+4zdJniFg=N^}N@sUm;6cY3`2o1din_PF_!K zj2>x%9(}dzaVl|)g;%M$#qil(74e<4cpSb@4wYdt=FVF%2|O6Pm2Y_|8};SWd%-Ph zaiZ8B3VO%@5lFWzRbDjdyVOiMZ+SO!6~ynxv6Oi>|1#FsqB~zCU;g zTn0q&Skw7HhtEprKQnk9?xD+V#wgjEyR8=fhq7A48Kh&u*92+tBHe7*zHz(IVL*4N z{Yn@p!0obn>GIvQZ_d{}h)8u0|MddlwvKaJ@jhKplK`;NG^Htb=C^6N)(WSI`0CG< zwrb6*u*$Cl0c1mWSQd?#_Y5&EvqepuZ%1XsMdX7WJcLyz{m!?2S)U|3-Sc|S%mVht$lvwU!mxg9z)GlMqF>olp zwiaqP=yGrN(B?*OI5l^(<3N?Ut(Sd(oo4n1cm~q?AR@@fcLJ!v8Nf(FHQ~`XCp50& z@9h{W&KW5)04j~eoRsEI{K)DVVOQ)KJ7cMC5BP^DEW_idTc$Q@Dc`i&L~mgVrX0;|u0qJidZ6 zy)uVl3vT6C-Q%d}-WI-}H@(XY#??2_$mwIsUuMoUC67=%@@?{; z&!rk2Mt-f2FQY1R`&|WZwEewOVl332sXnzmAzL`9t6lY%nE%PJT`K%^IynShR#rg` zk_)P=e@VYCcG=!|0KJ^rcs)V8#)?0}x1ZrA6VSL_Kt|>W>JTjn^Le2;hcVjQMu2?E zvMM&3L?;>^fLAI#^y-7qdkO>LkHd)xqD8E8pxV8bLamjZa1lloDy>ep=LVlEivOtH zgKBw3uqO>6o8G|mBOSAja1ot?h3U0X#aSuXRJ^Qy;#oOS#C@>PA3=lz%P7kw9wDtq zHE9Hq+H;pIr-8#5c$dXtiebq2>13nFB9p8LOpmGCPTa&>E8ETGy7WflC8XfiMEiTC z-`AepfYcDU&E1svUikA;%<;}IrJ`i`GA$LqJ1#F!1Ys&A4iZVOM}O+D87dZMLr=EzdZIIi5{L=p*@Z2+^IR0xS&+*y1|o$->%=~I6km2BfF z@*`JUHMGWhg{td<=F z>g&iS-li3<3@}o`r2hRw*y&4#Vdh`j%ND|h( z{KUmhg6Mv4Xt8ncFciKL$olEFn3Z1cx4yp_BT*w~C-zbjCi>-IqTlmdR40*>%y-5J z#sC8;!MOm42WR)m0;1*t@>XlwW_{F>swbTxI0T;JTP5qV=5{DQRp=>lu?OxWQ}8ii ztzv-gG~-%9dVQS#q?=kqR1&Ck<*F@_f98rD z6E65AjRkTi7AbK{-3s{lohMF4@2Z(+BoJ83h}rM-6v~KG2wyi`MuX`c6L=l)?{-Rr z<$IAaeu{3G2pefI_br>s`MbPnIT*~7(dD0(^&_zwty~))k!0)3_@9Pm8sL{u>zlDJ zt|pCk)+9s~$KQmsp}Q+3^QvKX55G(=T2?V=J> zDqy>Gxz%s|5k@$$LecaNlqALt!JAY_+s7T>zNemq_&bvIG|wb`mS47PV}x8Ze^feH zq{0=B-cENLE0Lz3pMARi_2Q&B1WviDoeB!g;T%p5kzo=`UH@`Sx=Q-p$4L(Sl3eG% zP70bDJJ6|2&$4>6xx4(CLFK>n5A29@M@?L47jSnogI1f|Aps{qAY--btTr+DbEMmS zJqIiG#3HDSsk&dK3Ka3l2iv%L(Bz{PKVqL13b;Vx{qv~V?kfdFR7ysyt+$TFe?#*>_Bc^E){O2*~f z7ABcQld(fOL#SjAGDmd(QqZniVQIB=Sx*(6MoXZ=dv?T;he#kBh)2I zR*MS)mXdvasE*B<$n@1CB3f!mT<;{-Y;Xz|H+=W_iI^zMMwuCp`EkLvy!QkN-DZ(q zkc_kP$=10a)b3FJhw7vRCiSQ^qnfK!sam5X;8OIrv)JUsrV6VyBv*E=6tQomaQ=-f zi!5IcK-9wxKBQSO5c&I6&^cgDFWn!(G=UHOD%#p?QA;VZ06*(To!%=NtCR;pFJH4J z{0w^2NA7rc4QV@Z^o`T#ud$iZ5-_atpx?XJTx=XRB0F$}{_42C#rt>00C@cC!BueS zD%*+qX1TU8Ub_AXpKCg6u71ssa^k98TZZ?q#FDSiIj~lGzIl*pz3_0=EV7~5CJ-)gcK)rM1aEb0=s?#aUj(7KaBiu!N z%}<|Yd!pFUGr%d~E79|QM(g7C$wEM5?Di@_m$#YPdhTB(S?eruXMfQXw{bi-L(S^r z;@Y=~D83B6g~V(_mV_(GZoAa^l#&m)w4rbS3=jOO9_z~|SHD22lWC8=bD`Fb3Mwq& zcOY3qlhCPK?Q?qJf3m$39LTaPXs}}N#)o_Y4Mr>dF}ahV8ct_Zjq0}S4%Scgf2AY4 zMfyC*k+wRW@x;H82ca?MAOP}{GC}rGRnht4Z`E-UOdV3OB)ZQm$$*j#dS|k!5D^*t zr+%F7*Vlkgn@)Fj-C8Yd(8cfA4K^x!|4fw=0*dS^%VH&pe|88x<`%@m6dNx|p1kV<_y*Gt+cl~fXs24xvoExm4_U)DzHNBjUE+0Kc4e0i!gulhy` zlP?^V1I2SzY=qg6ia!DB8^6o~)(M1p&3Ilf)qL6@EU1XExhs-`wZ0Uu<+`zwieq^A zx;o{GB?kD;p2^PL5uH!Kmt}dgU;@bP*y9b3#R3<5lJV@rs1IN#DkwcLHLk5C{d0bE z!NF&w+ai?fv%>w)=w@{EEe(NZy14X)E3B*BU+jg^eNs83{!cZU)Q4RiAF6|AdZ(h& zDuMHnZ?E13jYoFUJfq*FCMsJPeQodA?|CdVK%cA>C~1UGTcuRRPjvYn!nnBC$Kt(* zu2Ohs#tpXrjeu2DP1R|HqYfo4kxr^=I;r4iv?lzm&x%Pjs`L)V)^Cr|1!NyRsL@K< zww*hpD`x9ma(}0n4|Su25${bP+%|zzx2!cC%}ln)EpiPxM1E9n>~d;D<$UyJ;TN80 z|Mt-V54yslk1@r7Hjg%3;Vkwkiud*(;A%e1s{0j3mhr=Gjt{`DAiPVYYF>@RJQjQ6 zBBy04&1tmSpV4jYY-gq8B;~u+x4eF?`5eUu`@3qB0aQPKEdM@a$i%LDK&|B%{u!wF zMUxUD(^NH29#Qu+<)pR36e*-PX#@;1v?VW@AIs|cNdGFJ!v(3_jDUGxgnTL3pH`sR zr&QrM0l`0+M%sKxUp=_3*zmT)#kw*!Ff$aAv>n_$+r}3HhJ93tbw+{ActlnX+(fS?t>y*_%i=jXk#j5TJ-~WG|cW zt$lk{Wj3PpZk*>i&5YA?^j%A+Sh^$C+y*J+Bqe(BX7LZNV$+9E_*U!Jlx6s^G0Zb? zi?U2nloKHLbRo&5*RIvxH4!96zM+Vr!=#l)-xb=K}M zwv(U$Yj_j=sJ(SMn$TZbMgf_D$a$j6rMK*f{p}t3&5WKV*xh5dXkQ4IsMV@4vfby) zBf49y&QT)d222yXZT_QO75IL~c9qE)png2$*lL6M@xsw_UFo(VS$S$MV|JLeHo*g# zoX&@rrGi(V7i$PH#ayW#olC)A6Koe;w?dg zdvBDMrC?+lfUifuBY$ES^a3Q{_Q}a#wCGiAAzd(`Yb0r~8Wy}#BpU~m?CcqURGPA`I&nj$J>D<@rtM_^>zHg6ax9mRLA?4|zW&<+!6m9E zz2ql*)F*rT_$j6|Sn5L#)wnhrqbz(bUQC?^{@^#)SgCx)BK6s@gsj51llry072AXu zD7&dWGbN1`0KifCj|(9GPsEgDRz&feg;LpTvLxTv+m7VakFY*IqSq#YQDd|8Pu{bJ zxj)M~+mmSp@#f4Zj2>gD?<+lh)K7!~jx-Zh>qtQ<`|n%m#Z6Z;dgc@&-q1n&Z{+ha zC>NF}q+sdI^IQ3A)1ghmO=HN!an)Rl39aSx8<5*JNT`ORoAKP~;6Tkfz7hWR$Y!2* zNeYG=WRHT!@b?N5=AA+cIHQ`9MbUUd6PG&wf${HB!|l9Bs8u+lenOZP*+25o6UpTF z!bb1!#q!2DAPNI%HNm8Vx0 z)5h{oA)5tdoA2i4<`4jWfIwlg>oE6E#)^7t@{;`g5WHX*R^;H|U<*zrJs1P`7!m_8 z(6Is@vju=(4x!IU4l0v{iSJS85_4}xgon>AF4Bvj1<+z{JEOT&7+igl`-$MoCkAU% zyvwKC64V57LqkKcF2gbaj0Dng^7-&Obxd>gI3IqB%y1pHkyaF;CyG~BS3knwLQ*j9 zzlp30S0vKx05Bdif9gQb_V#uTVgqVtX$eZ!qT!PGGS?pX@D8(Vs4PMigO0xt6fBTG>t%)l2nY!=Qd1WSg3N7weQ!H*Wg-IlsjW~U@SYZv zXI*g;l+is`2Xp7-R*q@z5>g~90S&qZpMlcelv!%ISPBC01Vj))QmYIGC^9uio9=MT??wAkNvNX8f2uL z#VdY2`t{oSFsZ<7{@S<7B;x*droC5a#uzJgppZ!8w6wI;T#PZbxw#qlvC^4cvv7wY z*xqZ!Zt&YjbcU6!t*!k`rHO{BnwmKADgf=5ruDL)I+{V$P1fiBv@d#UB&h|pSW@Lh z0ydajYw}e6D@SDlmWG`Bw#K+K;wC_FLORJf)MMO+*UtYmJyvon#dW}zba2fxC=`$L zx&l0V^@?s0qtrG%Q=hH)F*^@F)L+0dDcHg|sX*}=wd%4DI+LLBvewc}jmZ%i=459V zndQCg!(ug9$f+#MY>mfk{TS`{7)9al@9*%5$Kd<-^78VL{ma{NLTL||oYrnpTq{*| zHqsAaVPU^8hBaBLT`W&UNhvRiW(`f)$T;5ssB0;|0fgp{*;yiy)Vm+eRAdt*|4P03 ztvzP)V&&ge{n*ly+LCj<3L+W6Y)!&kK-^(D?8AhT3r*k!NGO*^Xzj6tY7UEH)4y=G zd7CG071JM;*PTEsPJoq${Y*})D^q({PZ1hA`}1dT-C3#$$52aK5zP>GnWEXl?ac0H zA%_yqD9nC(sLz2Am*H1H@Hihab9dN9MY=tf%jB6>@$-QUch4I!5NuGC7@^#iJpK$V zh687$p-DV$+OI6zzEATRAW=L%Za$XyYFHZ;m8Xb7+X1`)7W86|C{cP2Py6y^LjJu9qBycj(MOvl3W?z$aj0yAC{DxbEW-GNCA}_dd06|t{6}`u z4BPVssFleJw|QMtl=8lE-7NVe5LGk@Tl$qRYi39H8u>~V%H3juVIBF!sY;eedy1pj zb@S=Wn(v}?QZq%|bW~{MwCxj|HxYS!KM1*77+4}HmU$INoh1~Cm~?WR7~vnw#$v0M z{N>69)agab3S;uPgcOC(ILD~w!zq@DNV@UgQL)(xXR}TamuZ?1=O|6>^v)c$u}s-; z#L-V_$&V$|7SQ=5?>utq!haV5u?(w!MEc{)Na8N8Zb>EXXo0=QhJ(?=18_z9Syci+ 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 530/734] 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 531/734] 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 532/734] 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 533/734] 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 534/734] 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 535/734] 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 536/734] 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 537/734] 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 538/734] 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 539/734] 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 540/734] 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 541/734] 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 542/734] 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 543/734] 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 544/734] 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 545/734] 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 546/734] 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 547/734] 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 548/734] 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 549/734] 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 550/734] 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 551/734] 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 552/734] 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 553/734] 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 554/734] 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 555/734] 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 556/734] 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 557/734] 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 558/734] 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 559/734] 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 560/734] 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 561/734] 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 562/734] 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 563/734] 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 564/734] 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 565/734] 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 566/734] 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 567/734] 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 568/734] 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 569/734] 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 570/734] 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 571/734] 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 572/734] 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 573/734] 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 574/734] 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 575/734] 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 576/734] 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 577/734] 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 578/734] 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 579/734] 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 580/734] 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 581/734] 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 582/734] 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 583/734] 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 584/734] 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 585/734] 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 586/734] 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 587/734] 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 588/734] 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 589/734] 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 590/734] 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 591/734] 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 592/734] 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 593/734] 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 594/734] 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 595/734] 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 596/734] 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 597/734] 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 598/734] 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 599/734] 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 600/734] 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 601/734] 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 602/734] 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 603/734] 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 604/734] 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 605/734] 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 606/734] 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 607/734] 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 608/734] 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 609/734] 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 610/734] 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 611/734] 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 612/734] 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 613/734] 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 614/734] 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 615/734] 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 616/734] 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 617/734] 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 618/734] 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 619/734] 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 620/734] 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 621/734] 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 622/734] 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 623/734] 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 624/734] 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 625/734] 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 626/734] 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 627/734] 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 628/734] 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 629/734] 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 920477bbb2d9fde761fc1c1321ea19bca7d58912 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 13:13:51 +0800 Subject: [PATCH 630/734] delete discovery from RustDesk_lan_peers.toml Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 8 ++++++-- src/flutter_ffi.rs | 4 ++++ src/ui.rs | 4 +--- src/ui_interface.rs | 7 +++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 8d4d58772..470b631ce 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -534,7 +534,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { () async { if (isLan) { - // TODO + bind.mainRemoveDiscovered(id: id); } else { final favs = (await bind.mainGetFav()).toList(); if (favs.remove(id)) { @@ -859,7 +859,11 @@ class DiscoveredPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async {})); + menuItems.add( + _removeAction(peer.id, () async { + await bind.mainLoadLanPeers(); + }, isLan: true), + ); return menuItems; } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 8a8bf4de4..23a65c2da 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -796,6 +796,10 @@ pub fn main_load_lan_peers() { }; } +pub fn main_remove_discovered(id: String) { + remove_discovered(id); +} + fn main_broadcast_message(data: &HashMap<&str, &str>) { let apps = vec![ flutter::APP_TYPE_DESKTOP_REMOTE, diff --git a/src/ui.rs b/src/ui.rs index 1b6838e46..a197cb257 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -413,9 +413,7 @@ impl UI { } fn remove_discovered(&mut self, id: String) { - let mut peers = config::LanPeers::load().peers; - peers.retain(|x| x.id != id); - config::LanPeers::store(&peers); + remove_discovered(id); } fn send_wol(&mut self, id: String) { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index dd111f86e..3b2ba0897 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -596,6 +596,13 @@ pub fn get_lan_peers() -> Vec> { .collect() } +#[inline] +pub fn remove_discovered(id: String) { + let mut peers = config::LanPeers::load().peers; + peers.retain(|x| x.id != id); + config::LanPeers::store(&peers); +} + #[inline] pub fn get_uuid() -> String { base64::encode(hbb_common::get_uuid()) From 0ad6bca9ceb87bcdd17354a29460ac34aec3fef5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 13:39:28 +0800 Subject: [PATCH 631/734] check existence for visibility of addFavAction/addToAb Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 38 ++++++++++------------- src/flutter_ffi.rs | 4 +++ 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 470b631ce..5b6120a02 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -745,12 +745,9 @@ class RecentPeerCard extends BasePeerCard { } if (gFFI.userModel.userName.isNotEmpty) { - // if (!gFFI.abModel.idContainBy(peer.id)) { - // menuItems.add(_addToAb(peer)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); @@ -797,12 +794,9 @@ class FavoritePeerCard extends BasePeerCard { })); if (gFFI.userModel.userName.isNotEmpty) { - // if (!gFFI.abModel.idContainBy(peer.id)) { - // menuItems.add(_addToAb(peer)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); @@ -843,19 +837,19 @@ 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 {})); + final inRecent = await bind.mainIsInRecentPeers(id: peer.id); + if (inRecent) { + 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)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 23a65c2da..0866ff739 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -726,6 +726,10 @@ pub fn main_peer_has_password(id: String) -> bool { peer_has_password(id) } +pub fn main_is_in_recent_peers(id: String) -> bool { + PeerConfig::peers().iter().any(|e| e.0 == id) +} + pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let peers: Vec> = PeerConfig::peers() From a9598e006a61a6eeee758c0031025b9a50eca0a1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 15:51:13 +0800 Subject: [PATCH 632/734] request elevation menu Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 10 ++++++++++ flutter/lib/mobile/widgets/dialog.dart | 7 +++---- flutter/lib/models/model.dart | 20 +++++++++++++++++++ src/client/io_loop.rs | 19 ++++++++++-------- src/flutter.rs | 7 +++++++ src/server/connection.rs | 16 +++++---------- src/ui/remote.rs | 2 ++ src/ui_session_interface.rs | 1 + 8 files changed, 59 insertions(+), 23 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4f9a227bd..bdf9c1f1d 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -598,6 +598,7 @@ class _ControlMenu extends StatelessWidget { hoverColor: _MenubarTheme.hoverBlueColor, ffi: ffi, menuChildren: [ + requestElevation(), osPassword(), transferFile(context), tcpTunneling(context), @@ -611,6 +612,15 @@ class _ControlMenu extends StatelessWidget { ]); } + requestElevation() { + final visible = ffi.elevationModel.showRequestMenu; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Request Elevation')), + ffi: ffi, + onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)); + } + osPassword() { return _MenuItemButton( child: Text(translate('OS Password')), diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 7e9a9879c..931999382 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -374,8 +374,7 @@ void showWaitUacDialog( )); } -void _showRequestElevationDialog( - String id, OverlayDialogManager dialogManager) { +void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) { RxString groupValue = ''.obs; RxString errUser = ''.obs; RxString errPwd = ''.obs; @@ -531,7 +530,7 @@ void showOnBlockDialog( dialogManager.show(tag: '$id-$type', (setState, close) { void submit() { close(); - _showRequestElevationDialog(id, dialogManager); + showRequestElevationDialog(id, dialogManager); } return CustomAlertDialog( @@ -553,7 +552,7 @@ void showElevationError(String id, String type, String title, String text, dialogManager.show(tag: '$id-$type', (setState, close) { void submit() { close(); - _showRequestElevationDialog(id, dialogManager); + showRequestElevationDialog(id, dialogManager); } return CustomAlertDialog( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f4efe2f08..eae41679f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -203,6 +203,8 @@ class FfiModel with ChangeNotifier { final peer_id = evt['peer_id'].toString(); await bind.sessionSwitchSides(id: peer_id); closeConnection(id: peer_id); + } else if (name == 'portable_service_running') { + parent.target?.elevationModel.onPortableServiceRunning(evt); } else if (name == "on_url_scheme_received") { final url = evt['url'].toString(); parseRustdeskUri(url); @@ -439,6 +441,7 @@ class FfiModel with ChangeNotifier { Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; handleResolutions(peerId, evt["resolutions"]); + parent.target?.elevationModel.onPeerInfo(_pi); } notifyListeners(); } @@ -1395,6 +1398,21 @@ class RecordingModel with ChangeNotifier { } } +class ElevationModel with ChangeNotifier { + WeakReference parent; + ElevationModel(this.parent); + bool _running = false; + bool _canElevate = false; + bool get showRequestMenu => _canElevate && !_running; + onPeerInfo(PeerInfo pi) { + _canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false; + } + + onPortableServiceRunning(Map evt) { + _running = evt['running'] == 'true'; + } +} + enum ConnType { defaultConn, fileTransfer, portForward, rdp } /// Flutter state manager and data communication with the Rust core. @@ -1420,6 +1438,7 @@ class FFI { late final QualityMonitorModel qualityMonitorModel; // session late final RecordingModel recordingModel; // session late final InputModel inputModel; // session + late final ElevationModel elevationModel; // session FFI() { imageModel = ImageModel(WeakReference(this)); @@ -1436,6 +1455,7 @@ class FFI { qualityMonitorModel = QualityMonitorModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this)); inputModel = InputModel(WeakReference(this)); + elevationModel = ElevationModel(WeakReference(this)); } /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index b51c481a5..1c7788193 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -56,6 +56,7 @@ pub struct Remote { data_count: Arc, frame_count: Arc, video_format: CodecFormat, + elevation_requested: bool, } impl Remote { @@ -87,6 +88,7 @@ impl Remote { video_format: CodecFormat::Unknown, stop_voice_call_sender: None, voice_call_request_timestamp: None, + elevation_requested: false, } } @@ -686,6 +688,7 @@ impl Remote { let mut msg = Message::new(); msg.set_misc(misc); allow_err!(peer.send(&msg).await); + self.elevation_requested = true; } Data::ElevateWithLogon(username, password) => { let mut request = ElevationRequest::new(); @@ -699,6 +702,7 @@ impl Remote { let mut msg = Message::new(); msg.set_misc(misc); allow_err!(peer.send(&msg).await); + self.elevation_requested = true; } Data::NewVoiceCall => { let msg = new_voice_call_request(true); @@ -1181,7 +1185,8 @@ impl Remote { } } Some(misc::Union::PortableServiceRunning(b)) => { - if b { + self.handler.portable_service_running(b); + if self.elevation_requested && b { self.handler.msgbox( "custom-nocancel-success", "Successful", @@ -1253,14 +1258,12 @@ impl Remote { } } } - Some(message::Union::PeerInfo(pi)) => { - match pi.conn_id { - crate::SYNC_PEER_INFO_DISPLAYS => { - self.handler.set_displays(&pi.displays); - } - _ => {} + Some(message::Union::PeerInfo(pi)) => match pi.conn_id { + crate::SYNC_PEER_INFO_DISPLAYS => { + self.handler.set_displays(&pi.displays); } - } + _ => {} + }, _ => {} } } diff --git a/src/flutter.rs b/src/flutter.rs index ea73eb925..2f660775f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -572,6 +572,13 @@ impl InvokeUiSession for FlutterHandler { self.push_event("switch_back", [("peer_id", peer_id)].into()); } + fn portable_service_running(&self, running: bool) { + self.push_event( + "portable_service_running", + [("running", running.to_string().as_str())].into(), + ); + } + fn on_voice_call_started(&self) { self.push_event("on_voice_call_started", [].into()); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 85fcb676b..b2e198681 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1552,7 +1552,6 @@ impl Connection { .err() .map_or("".to_string(), |e| e.to_string()); } - self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1571,7 +1570,6 @@ impl Connection { .err() .map_or("".to_string(), |e| e.to_string()); } - self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1936,13 +1934,11 @@ impl Connection { let p = &mut self.portable; if running != p.last_running { p.last_running = running; - if running && p.elevation_requested { - let mut misc = Misc::new(); - misc.set_portable_service_running(running); - let mut msg = Message::new(); - msg.set_misc(misc); - self.inner.send(msg.into()); - } + let mut misc = Misc::new(); + misc.set_portable_service_running(running); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); } let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); if p.last_uac != uac { @@ -2166,7 +2162,6 @@ pub struct PortableState { pub last_foreground_window_elevated: bool, pub last_running: bool, pub is_installed: bool, - pub elevation_requested: bool, } #[cfg(windows)] @@ -2177,7 +2172,6 @@ impl Default for PortableState { last_uac: Default::default(), last_foreground_window_elevated: Default::default(), last_running: Default::default(), - elevation_requested: Default::default(), } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 7b31c84e9..c6e0229b2 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -277,6 +277,8 @@ impl InvokeUiSession for SciterHandler { fn switch_back(&self, _id: &str) {} + fn portable_service_running(&self, _running: bool) {} + fn on_voice_call_started(&self) { self.call("onVoiceCallStart", &make_args!()); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index fd5a7d9c0..f726ed526 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -805,6 +805,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); + fn portable_service_running(&self, running: bool); fn on_voice_call_started(&self); fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); From c3c4505132b3e7109361d02a1b6fa69a7377bd9b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 24 Feb 2023 14:15:54 +0800 Subject: [PATCH 633/734] 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 634/734] 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()) ], ), ), From 47a514a41670b2a4f9134219bf2854dd412c0d0f Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 16:20:00 +0800 Subject: [PATCH 635/734] optimize menubar code Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index bdf9c1f1d..8710d27c3 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1101,7 +1101,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { await bind.sessionSetImageQuality(id: widget.id, value: value); } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, child: Text(translate('Image Quality')), menuChildren: [ _RadioMenuButton( @@ -1135,7 +1136,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { }, ffi: widget.ffi, ), - ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList(), + ], ); }); } @@ -1310,7 +1311,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { bind.sessionChangePreferCodec(id: widget.id); } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, child: Text(translate('Codec')), menuChildren: [ _RadioMenuButton( @@ -1341,7 +1343,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { onChanged: onChanged, ffi: widget.ffi, ), - ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList()); + ]); }); } @@ -1373,7 +1375,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { } } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, menuChildren: resolutions .map((e) => _RadioMenuButton( value: '${e.width}x${e.height}', @@ -1381,8 +1384,6 @@ class _DisplayMenuState extends State<_DisplayMenu> { onChanged: onChanged, ffi: widget.ffi, child: Text('${e.width}x${e.height}'))) - .toList() - .map((e) => _buildPointerTrackWidget(e, widget.ffi)) .toList(), child: Text(translate("Resolution"))); } @@ -1869,6 +1870,28 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> { } } +class _SubmenuButton extends StatelessWidget { + final List menuChildren; + final Widget? child; + final FFI ffi; + const _SubmenuButton({ + Key? key, + required this.menuChildren, + required this.child, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SubmenuButton( + key: key, + child: child, + menuChildren: + menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(), + ); + } +} + class _MenuItemButton extends StatelessWidget { final VoidCallback? onPressed; final Widget? trailingIcon; From 2a71b65a618364aaf9f30f75fc428a2fc07f8940 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 19:06:37 +0800 Subject: [PATCH 636/734] add missing insertLock menu Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 8710d27c3..37bbbd66a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -606,6 +606,7 @@ class _ControlMenu extends StatelessWidget { Divider(), ctrlAltDel(), restart(), + insertLock(), blockUserInput(), switchSides(), refresh(), @@ -789,6 +790,16 @@ class _ControlMenu extends StatelessWidget { onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); } + insertLock() { + final perms = ffi.ffiModel.permissions; + final visible = perms['keyboard'] != false; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Insert Lock')), + ffi: ffi, + onPressed: () => bind.sessionLockScreen(id: id)); + } + blockUserInput() { final perms = ffi.ffiModel.permissions; final pi = ffi.ffiModel.pi; From c6f8df36a2018ea3a20cb692bf439ea989c6a6cc Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 24 Feb 2023 19:55:46 +0800 Subject: [PATCH 637/734] update options after login Signed-off-by: fufesou --- src/server/connection.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index b2e198681..898939b62 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -126,6 +126,7 @@ pub struct Connection { origin_resolution: HashMap, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, + options_in_login: Option, } impl ConnInner { @@ -233,6 +234,7 @@ impl Connection { audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, + options_in_login: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -921,6 +923,9 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_login_response(res); self.send(msg_out).await; + if let Some(o) = self.options_in_login.take() { + self.update_options(&o).await; + } if let Some((dir, show_hidden)) = self.file_transfer.clone() { let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { &dir @@ -1106,8 +1111,7 @@ 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() { - // It may not be a good practice to update all options here. - self.update_options(o).await; + self.options_in_login = Some(o.clone()); if let Some(q) = o.video_codec_state.clone().take() { scrap::codec::Encoder::update_video_encoder( self.inner.id(), @@ -1697,7 +1701,8 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } - async fn update_options_without_auth(&mut self, o: &OptionMessage) { + async fn update_options(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { let image_quality; if let ImageQuality::NotSet = q { @@ -1728,12 +1733,6 @@ impl Connection { 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; @@ -1862,12 +1861,6 @@ impl Connection { } } - async fn update_options(&mut self, o: &OptionMessage) { - log::info!("Option update: {:?}", 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) { log::info!("#{} Connection closed: {}", self.inner.id(), reason); if lock && self.lock_after_session_end && self.keyboard { From 1e387ce01917c7951f2b88dcb59c08bd59627cbf Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 24 Feb 2023 20:24:31 +0800 Subject: [PATCH 638/734] simple refact Signed-off-by: fufesou --- src/platform/linux.rs | 120 +++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 08e343d49..47184e796 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,14 +1,22 @@ 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, anyhow::anyhow, bail, log, message_proto::Resolution}; +use hbb_common::{ + allow_err, + anyhow::anyhow, + bail, + libc::{c_char, c_int, c_long, c_void}, + log, + message_proto::Resolution, +}; use std::{ cell::RefCell, - path::PathBuf, + path::{Path, PathBuf}, + process::{Child, Command}, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, + time::{Duration, Instant}, }; use xrandr_parser::Parser; @@ -162,10 +170,29 @@ fn start_uinput_service() { }); } -fn stop_server(server: &mut Option) { +#[inline] +fn try_start_server_(user: Option<(String, String)>) -> ResultType> { + if user.is_some() { + run_as_user(vec!["--server"], user) + } else { + Ok(Some(crate::run_me(vec!["--server"])?)) + } +} + +#[inline] +fn start_server(user: Option<(String, String)>, server: &mut Option) { + match try_start_server_(user) { + Ok(ps) => *server = ps, + Err(err) => { + log::error!("Failed to start server: {}", err); + } + } +} + +fn stop_server(server: &mut Option) { if let Some(mut ps) = server.take() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); + std::thread::sleep(Duration::from_millis(30)); match ps.try_wait() { Ok(Some(_status)) => {} Ok(None) => { @@ -182,7 +209,7 @@ fn set_x11_env(uid: &str) { let mut auth = get_env_tries("XAUTHORITY", uid, 10); // auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468 if auth.is_empty() || uid == "0" { - auth = if std::path::Path::new(&gdm).exists() { + auth = if Path::new(&gdm).exists() { gdm } else { let username = get_active_username(); @@ -190,7 +217,7 @@ fn set_x11_env(uid: &str) { format!("/{}/.Xauthority", username) } else { let tmp = format!("/home/{}/.Xauthority", username); - if std::path::Path::new(&tmp).exists() { + if Path::new(&tmp).exists() { tmp } else { format!("/var/lib/{}/.Xauthority", username) @@ -223,8 +250,8 @@ fn should_start_server( uid: &mut String, cur_uid: String, cm0: &mut bool, - last_restart: &mut std::time::Instant, - server: &mut Option, + last_restart: &mut Instant, + server: &mut Option, ) -> bool { let cm = get_cm(); let mut start_new = false; @@ -235,8 +262,8 @@ fn should_start_server( } if let Some(ps) = server.as_mut() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - *last_restart = std::time::Instant::now(); + std::thread::sleep(Duration::from_millis(30)); + *last_restart = Instant::now(); } } else if !cm && ((*cm0 && last_restart.elapsed().as_secs() > 60) @@ -247,8 +274,8 @@ fn should_start_server( // and x server get displays failure issue if let Some(ps) = server.as_mut() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - *last_restart = std::time::Instant::now(); + std::thread::sleep(Duration::from_millis(30)); + *last_restart = Instant::now(); log::info!("restart server"); } } @@ -267,6 +294,13 @@ fn should_start_server( start_new } +// to-do: stop_server(&mut user_server); may not stop child correctly +// stop_rustdesk_servers() is just a temp solution here. +fn force_stop_server() { + stop_rustdesk_servers(); + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); +} + pub fn start_os_service() { stop_rustdesk_servers(); start_uinput_service(); @@ -274,8 +308,8 @@ pub fn start_os_service() { let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); let mut uid = "".to_owned(); - let mut server: Option = None; - let mut user_server: Option = None; + let mut server: Option = None; + let mut user_server: Option = None; if let Err(err) = ctrlc::set_handler(move || { r.store(false, Ordering::SeqCst); }) { @@ -283,12 +317,13 @@ pub fn start_os_service() { } let mut cm0 = false; - let mut last_restart = std::time::Instant::now(); + let mut last_restart = Instant::now(); while running.load(Ordering::SeqCst) { let (cur_uid, cur_user) = get_active_user_id_name(); let is_wayland = current_is_wayland(); if cur_user == "root" || !is_wayland { + // try kill subprocess "--server" stop_server(&mut user_server); // try start subprocess "--server" if should_start_server( @@ -299,16 +334,8 @@ pub fn start_os_service() { &mut last_restart, &mut server, ) { - // to-do: stop_server(&mut user_server); may not stop child correctly - // stop_rustdesk_servers() is just a temp solution here. - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); - match crate::run_me(vec!["--server"]) { - Ok(ps) => server = Some(ps), - Err(err) => { - log::error!("Failed to start server: {}", err); - } - } + force_stop_server(); + start_server(None, &mut server); } } else if cur_user != "" { if cur_user != "gdm" { @@ -324,23 +351,16 @@ pub fn start_os_service() { &mut last_restart, &mut user_server, ) { - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); - match run_as_user(vec!["--server"], Some((cur_uid, cur_user))) { - Ok(ps) => user_server = ps, - Err(err) => { - log::error!("Failed to start server: {}", err); - } - } + force_stop_server(); + start_server(Some((cur_uid, cur_user)), &mut user_server); } } } else { - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + force_stop_server(); stop_server(&mut user_server); stop_server(&mut server); } - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); } if let Some(ps) = user_server.take().as_mut() { @@ -362,7 +382,7 @@ pub fn get_active_userid() -> String { } fn get_cm() -> bool { - if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() { + if let Ok(output) = Command::new("ps").args(vec!["aux"]).output() { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains(&format!( "{} --cm", @@ -380,7 +400,7 @@ fn get_cm() -> bool { fn get_display() -> String { let user = get_active_username(); log::debug!("w {}", &user); - if let Ok(output) = std::process::Command::new("w").arg(&user).output() { + if let Ok(output) = Command::new("w").arg(&user).output() { for line in String::from_utf8_lossy(&output.stdout).lines() { log::debug!(" {}", line); let mut iter = line.split_whitespace(); @@ -395,7 +415,7 @@ fn get_display() -> String { // above not work for gdm user log::debug!("ls -l /tmp/.X11-unix/"); let mut last = "".to_owned(); - if let Ok(output) = std::process::Command::new("ls") + if let Ok(output) = Command::new("ls") .args(vec!["-l", "/tmp/.X11-unix/"]) .output() { @@ -474,10 +494,7 @@ fn is_opensuse() -> bool { false } -pub fn run_as_user( - arg: Vec<&str>, - user: Option<(String, String)>, -) -> ResultType> { +pub fn run_as_user(arg: Vec<&str>, user: Option<(String, String)>) -> ResultType> { let (uid, username) = match user { Some(id_name) => id_name, None => get_active_user_id_name(), @@ -491,7 +508,7 @@ pub fn run_as_user( args.insert(0, "-E"); } - let task = std::process::Command::new("sudo").args(args).spawn()?; + let task = Command::new("sudo").args(args).spawn()?; Ok(Some(task)) } @@ -553,10 +570,7 @@ pub fn get_default_pa_source() -> Option<(String, String)> { } pub fn lock_screen() { - std::process::Command::new("xdg-screensaver") - .arg("lock") - .spawn() - .ok(); + Command::new("xdg-screensaver").arg("lock").spawn().ok(); } pub fn toggle_blank_screen(_v: bool) { @@ -577,7 +591,7 @@ fn get_env_tries(name: &str, uid: &str, n: usize) -> String { if !x.is_empty() { return x; } - std::thread::sleep(std::time::Duration::from_millis(300)); + std::thread::sleep(Duration::from_millis(300)); } "".to_owned() } @@ -604,12 +618,12 @@ pub fn quit_gui() { pub fn check_super_user_permission() -> ResultType { let file = "/usr/share/rustdesk/files/polkit"; let arg; - if std::path::Path::new(file).is_file() { + if Path::new(file).is_file() { arg = file; } else { arg = "echo"; } - let status = std::process::Command::new("pkexec").arg(arg).status()?; + let status = Command::new("pkexec").arg(arg).status()?; Ok(status.success() && status.code() == Some(0)) } @@ -684,7 +698,7 @@ pub fn current_resolution(name: &str) -> ResultType { } pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { - std::process::Command::new("xrandr") + Command::new("xrandr") .args(vec![ "--output", name, From 8d726f53aabf33652bb8efaa190bb24ca7093a28 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 24 Feb 2023 23:38:23 +0800 Subject: [PATCH 639/734] better mouse position Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 9a5b06b14..fca73eac7 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -459,17 +459,22 @@ class InputModel { } evt['type'] = type; if (isDesktop) { - y = y - stateGlobal.tabBarHeight; + y = y - stateGlobal.tabBarHeight - stateGlobal.windowBorderWidth.value; + x -= stateGlobal.windowBorderWidth.value; } final canvasModel = parent.target!.canvasModel; + final nearThr = 3; + var nearRight = (canvasModel.size.width - x) < nearThr; + var nearBottom = (canvasModel.size.height - y) < nearThr; + final ffiModel = parent.target!.ffiModel; if (isMove) { canvasModel.moveDesktopMouse(x, y); } final d = ffiModel.display; + final imageWidth = d.width * canvasModel.scale; + final imageHeight = d.height * canvasModel.scale; if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { - final imageWidth = d.width * canvasModel.scale; - final imageHeight = d.height * canvasModel.scale; x += imageWidth * canvasModel.scrollX; y += imageHeight * canvasModel.scrollY; @@ -487,6 +492,15 @@ class InputModel { x /= canvasModel.scale; y /= canvasModel.scale; + if (canvasModel.scale > 0 && canvasModel.scale < 1) { + final step = 1.0 / canvasModel.scale - 1; + if (nearRight) { + x += step; + } + if (nearBottom) { + y += step; + } + } x += d.x; y += d.y; From ac2c7115344e1065cfc77efc8d6f516a2b660b58 Mon Sep 17 00:00:00 2001 From: ilGigioVr88 Date: Fri, 24 Feb 2023 17:04:05 +0100 Subject: [PATCH 640/734] Update it.rs --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 7256b13d8..04b2488a7 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -454,7 +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", ""), + ("Codec", "Codec"), + ("Resolution", "Risoluzione"), ].iter().cloned().collect(); } From 0a51fff04c984355efcc30a1d5130d0174a591cc Mon Sep 17 00:00:00 2001 From: Michal Witek Date: Fri, 24 Feb 2023 17:19:33 +0100 Subject: [PATCH 641/734] Added additional intent-filter --- flutter/android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 9b25f4973..ede6353ef 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ android:exported="false"> + From d76a1adfcce59652e3a17ba2d7dff6536ae9fdfa Mon Sep 17 00:00:00 2001 From: sjpark Date: Sat, 25 Feb 2023 11:41:02 +0900 Subject: [PATCH 642/734] swap key update --- .../lib/desktop/widgets/remote_menubar.dart | 18 ++++++++++++++++++ src/ui/header.tis | 4 ++-- src/ui_session_interface.rs | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4f9a227bd..329df4e19 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -895,6 +895,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { disableClipboard(), lockAfterSessionEnd(), privacyMode(), + swapKey(), ]); } @@ -1501,6 +1502,23 @@ class _DisplayMenuState extends State<_DisplayMenu> { ffi: widget.ffi, child: Text(translate('Privacy mode'))); } + + swapKey() { + final visible = perms['keyboard'] != false && + ((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) || + (!Platform.isMacOS && pi.platform == kPeerPlatformMacOS)); + if (!visible) return Offstage(); + final option = 'allow_swap_key'; + 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('Swap control-command key'))); + } } class _KeyboardMenu extends StatelessWidget { diff --git a/src/ui/header.tis b/src/ui/header.tis index 01808a156..257ba417e 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -156,7 +156,6 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Legacy mode')}
  • {svg_checkmark}{translate('Map mode')}
  • -
  • {svg_checkmark}{translate('Swap Control-Command Key')}
  • ; } @@ -199,6 +198,7 @@ class Header: Reactor.Component { {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ?
  • {svg_checkmark}{translate('Swap control-command key')}
  • : ""} ; } @@ -441,7 +441,7 @@ function toggleMenuState() { for (var el in $$(menu#keyboard-options>li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } - for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) { + for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key"]) { var el = self.select('#' + id); if (el) { var value = handler.get_toggle_option(id); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 37367c191..f764aa3ed 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1010,6 +1010,7 @@ impl Interface for Session { handle_test_delay(t, peer).await; } } + fn swap_modifier_mouse(&self, msg : &mut hbb_common::protos::message::MouseEvent) { let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string()); if allow_swap_key { From 218ce22fbd6f7d0d1c4acc9f03a10bb5419914e6 Mon Sep 17 00:00:00 2001 From: Integral <71180087+Integral-Tech@users.noreply.github.com> Date: Sat, 25 Feb 2023 14:25:35 +0800 Subject: [PATCH 643/734] Update cn.rs Some small fixes --- src/lang/cn.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 818e63203..fd2e04c53 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -38,7 +38,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "停止服务"), ("Change ID", "更改 ID"), ("Your new ID", "你的新 ID"), - ("length %min% to %max%", "长度在 %min 与 %max 之间"), + ("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 之间。"), @@ -137,7 +137,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to connect to rendezvous server", "连接注册服务器失败"), ("Please try later", "请稍后再试"), ("Remote desktop is offline", "远程电脑处于离线状态"), - ("Key mismatch", "密钥不匹配"), + ("Key mismatch", "Key 不匹配"), ("Timeout", "连接超时"), ("Failed to connect to relay server", "无法连接到中继服务器"), ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), From abeb2058eeb3e05691721c421913116f369b5197 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Sat, 25 Feb 2023 09:44:23 +0100 Subject: [PATCH 644/734] implemented shrinking transfers --- .../lib/desktop/pages/file_manager_page.dart | 291 +++++++++--------- 1 file changed, 144 insertions(+), 147 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 39d66f568..a709ccba5 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -153,9 +153,17 @@ class _FileManagerPageState extends State backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Row( children: [ - Flexible(flex: 3, child: body(isLocal: true)), - Flexible(flex: 3, child: body(isLocal: false)), - Flexible(flex: 2, child: statusList()) + Flexible( + flex: 3, + child: body(isLocal: true), + ), + Flexible( + flex: 3, + child: body(isLocal: false), + ), + model.jobTable.isEmpty + ? SizedBox() + : Flexible(flex: 2, child: statusList()) ], ), ); @@ -546,157 +554,146 @@ class _FileManagerPageState extends State /// watch transfer status Widget statusList() { return PreferredSize( - preferredSize: const Size(200, double.infinity), - 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(15.0), + 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), + 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(15.0), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Transform.rotate( + angle: item.isRemote ? pi : 0, + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + ).paddingOnly(left: 15), + 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, + ).paddingSymmetric(vertical: 10), + ), + Text( + '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + ), + 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: 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), + ), + ], ), ), - child: Column( - mainAxisSize: MainAxisSize.min, + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Transform.rotate( - angle: item.isRemote ? pi : 0, - child: SvgPicture.asset( - "assets/arrow.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, - ), - ).paddingOnly(left: 15), - const SizedBox( - width: 16.0, + Offstage( + offstage: item.state != JobState.paused, + child: MenuButton( + onPressed: () { + model.resumeJob(item.id); + }, + child: SvgPicture.asset( + "assets/refresh.svg", + color: Colors.white, ), - 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, - ).paddingSymmetric(vertical: 10), - ), - Text( - '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), - 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: 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), - ), - ], - ), - ), - 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: Colors.white, - ), - color: MyTheme.accent, - hoverColor: MyTheme.accent80, - ), - ), - MenuButton( - padding: EdgeInsets.only(right: 15), - child: SvgPicture.asset( - "assets/close.svg", - color: Colors.white, - ), - onPressed: () { - model.jobTable.removeAt(index); - model.cancelJob(item.id); - }, - color: MyTheme.accent, - hoverColor: MyTheme.accent80, - ), - ], - ), - ], + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), + ), + MenuButton( + padding: EdgeInsets.only(right: 15), + child: SvgPicture.asset( + "assets/close.svg", + color: Colors.white, + ), + onPressed: () { + model.jobTable.removeAt(index); + model.cancelJob(item.id); + }, + color: MyTheme.accent, + hoverColor: MyTheme.accent80, ), ], - ).paddingSymmetric(vertical: 10), - ), - ); - }, - itemCount: model.jobTable.length, - ), + ), + ], + ), + ], + ).paddingSymmetric(vertical: 10), ), - )); + ); + }, + itemCount: model.jobTable.length, + ), + ), + ), + ); } Widget headTools(bool isLocal) { From 75ecb66576226292b9d4845c6e91b5e9d8fb323b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Sat, 25 Feb 2023 09:50:40 +0100 Subject: [PATCH 645/734] improved light mode send/receive button readability --- flutter/lib/desktop/pages/file_manager_page.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index a709ccba5..96ab4044f 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1000,7 +1000,9 @@ class _FileManagerPageState extends State textAlign: TextAlign.right, style: TextStyle( color: selectedItems.length == 0 - ? MyTheme.darkGray + ? Theme.of(context).brightness == Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray : Colors.white, ), ) @@ -1009,7 +1011,9 @@ class _FileManagerPageState extends State child: SvgPicture.asset( "assets/arrow.svg", color: selectedItems.length == 0 - ? MyTheme.darkGray + ? Theme.of(context).brightness == Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray : Colors.white, alignment: Alignment.bottomRight, ), @@ -1018,14 +1022,18 @@ class _FileManagerPageState extends State ? SvgPicture.asset( "assets/arrow.svg", color: selectedItems.length == 0 - ? MyTheme.darkGray + ? Theme.of(context).brightness == Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray : Colors.white, ) : Text( translate('Receive'), style: TextStyle( color: selectedItems.length == 0 - ? MyTheme.darkGray + ? Theme.of(context).brightness == Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray : Colors.white, ), ), From eb39cc5da189e3c67c5b270dc38f1c18c9248ce4 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Sat, 25 Feb 2023 10:08:34 +0100 Subject: [PATCH 646/734] fix button flicker --- flutter/lib/desktop/pages/file_manager_page.dart | 2 +- flutter/lib/desktop/widgets/menu_button.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 96ab4044f..5df9c9a80 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -678,7 +678,7 @@ class _FileManagerPageState extends State model.cancelJob(item.id); }, color: MyTheme.accent, - hoverColor: MyTheme.accent80, + hoverColor: MyTheme.button, ), ], ), diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index df2c48ab4..17b160fed 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -37,7 +37,7 @@ class _MenuButtonState extends State { message: widget.tooltip, child: Material( type: MaterialType.transparency, - child: Ink( + child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(_borderRadius), color: _isHover ? widget.hoverColor : widget.color, From f71d4e9e815edf2486f01158250ef50873f5f1b4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 25 Feb 2023 18:39:27 +0800 Subject: [PATCH 647/734] initialize vdi with .devcontainer --- vdi/README.md | 1 + vdi/host/.devcontainer/Dockerfile | 16 + vdi/host/.devcontainer/devcontainer.json | 27 + vdi/host/.gitignore | 1 + vdi/host/Cargo.lock | 1185 ++++++++++++++++++++++ vdi/host/Cargo.toml | 9 + vdi/host/README.md | 1 + vdi/host/src/main.rs | 2 + 8 files changed, 1242 insertions(+) create mode 100644 vdi/README.md create mode 100644 vdi/host/.devcontainer/Dockerfile create mode 100644 vdi/host/.devcontainer/devcontainer.json create mode 100644 vdi/host/.gitignore create mode 100644 vdi/host/Cargo.lock create mode 100644 vdi/host/Cargo.toml create mode 100644 vdi/host/README.md create mode 100644 vdi/host/src/main.rs diff --git a/vdi/README.md b/vdi/README.md new file mode 100644 index 000000000..85e6ff194 --- /dev/null +++ b/vdi/README.md @@ -0,0 +1 @@ +# WIP diff --git a/vdi/host/.devcontainer/Dockerfile b/vdi/host/.devcontainer/Dockerfile new file mode 100644 index 000000000..f02042771 --- /dev/null +++ b/vdi/host/.devcontainer/Dockerfile @@ -0,0 +1,16 @@ +FROM rockylinux:9.1 +ENV HOME=/home/vscode +ENV WORKDIR=$HOME/rustdesk/vdi/host + +# https://ciq.co/blog/top-10-things-to-do-after-rocky-linux-9-install/ also gpu driver install +WORKDIR $HOME +RUN dnf -y install epel-release +RUN dnf config-manager --set-enabled crb +RUN dnf -y install cargo libvpx-devel opus-devel usbredir-devel git cmake gcc-c++ pkg-config nasm yasm ninja-build automake libtool libva-devel libvdpau-devel llvm-devel +WORKDIR / + +RUN git clone https://chromium.googlesource.com/libyuv/libyuv +WORKDIR /libyuv +RUN cmake . -DCMAKE_INSTALL_PREFIX=/user +RUN make -j4 && make install +WORKDIR / \ No newline at end of file diff --git a/vdi/host/.devcontainer/devcontainer.json b/vdi/host/.devcontainer/devcontainer.json new file mode 100644 index 000000000..02c6e589b --- /dev/null +++ b/vdi/host/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +{ + "name": "rustdesk", + "build": { + "dockerfile": "./Dockerfile", + "context": "." + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/vscode/rustdesk", + "customizations": { + "vscode": { + "extensions": [ + "vadimcn.vscode-lldb", + "mutantdino.resourcemonitor", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates", + "mhutchie.git-graph", + "eamodio.gitlens" + ], + "settings": { + "files.watcherExclude": { + "**/target/**": true + } + } + } + } +} \ No newline at end of file diff --git a/vdi/host/.gitignore b/vdi/host/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/vdi/host/.gitignore @@ -0,0 +1 @@ +/target diff --git a/vdi/host/Cargo.lock b/vdi/host/Cargo.lock new file mode 100644 index 000000000..d4254717e --- /dev/null +++ b/vdi/host/Cargo.lock @@ -0,0 +1,1185 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-broadcast" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" +dependencies = [ + "easy-parallel", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-broadcast" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot", +] + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "concurrent-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "easy-parallel" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" + +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "futures" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" + +[[package]] +name = "futures-executor" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + +[[package]] +name = "futures-task" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" + +[[package]] +name = "futures-util" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libusb1-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "ordered-stream" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qemu-display" +version = "0.1.0" +source = "git+https://gitlab.com/marcandre.lureau/qemu-display#544a4075615702abf414cd2d63bbb6a9ca10d0ea" +dependencies = [ + "async-broadcast 0.3.4", + "async-lock", + "async-trait", + "cfg-if", + "derivative", + "enumflags2", + "futures", + "futures-util", + "libc", + "log", + "once_cell", + "serde", + "serde_bytes", + "serde_repr", + "uds_windows", + "usbredirhost", + "windows", + "zbus", + "zvariant", +] + +[[package]] +name = "qemu-rustdesk" +version = "0.1.0" +dependencies = [ + "qemu-display", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rusb" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703aa035c21c589b34fb5136b12e68fc8dcf7ea46486861381361dd8ebf5cee0" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + +[[package]] +name = "serde_bytes" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_repr" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" + +[[package]] +name = "toml_edit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" +dependencies = [ + "indexmap", + "nom8", + "toml_datetime", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "usbredirhost" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87485e4dfeb0176203afd1086f11ed2ead837053143b12b6eed55c598e9393d5" +dependencies = [ + "libc", + "rusb", + "usbredirhost-sys", + "usbredirparser", +] + +[[package]] +name = "usbredirhost-sys" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b27c305da1f7601b665d68948bcfaf9909d443bec94510ab776118ab8afc2c7d" +dependencies = [ + "libusb1-sys", + "pkg-config", + "usbredirparser-sys", +] + +[[package]] +name = "usbredirparser" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f8b5241d7cbb3e08b4677212a9ac001f116f50731c2737d16129a84ecf6a56" +dependencies = [ + "libc", + "usbredirparser-sys", +] + +[[package]] +name = "usbredirparser-sys" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b0e834e187916fc762bccdc9d64e454a0ee58b134f8f7adab321141e8e0d91" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "zbus" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ce2de393c874ba871292e881bf3c13a0d5eb38170ebab2e50b4c410eaa222b" +dependencies = [ + "async-broadcast 0.4.1", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde-xml-rs", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13d08f5dc6cf725b693cb6ceacd43cd430ec0664a879188f29e7d7dcd98f96d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zvariant" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903169c05b9ab948ee93fefc9127d08930df4ce031d46c980784274439803e51" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "serde_bytes", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce76636e8fab7911be67211cf378c252b115ee7f2bae14b18b84821b39260b5" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] diff --git a/vdi/host/Cargo.toml b/vdi/host/Cargo.toml new file mode 100644 index 000000000..62c964122 --- /dev/null +++ b/vdi/host/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "qemu-rustdesk" +version = "0.1.0" +authors = ["rustdesk "] +edition = "2021" + + +[dependencies] +qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" } \ No newline at end of file diff --git a/vdi/host/README.md b/vdi/host/README.md new file mode 100644 index 000000000..3b29a10e3 --- /dev/null +++ b/vdi/host/README.md @@ -0,0 +1 @@ +# RustDesk protocol on QEMU D-Bus display diff --git a/vdi/host/src/main.rs b/vdi/host/src/main.rs new file mode 100644 index 000000000..f79c691f0 --- /dev/null +++ b/vdi/host/src/main.rs @@ -0,0 +1,2 @@ +fn main() { +} From b8b3e996026fa47352feb9bcb30c45cc5b4a1442 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 25 Feb 2023 22:39:12 +0800 Subject: [PATCH 648/734] macos, periodically check if current display is changed Signed-off-by: fufesou --- src/platform/macos.mm | 3 +-- src/platform/macos.rs | 8 ++++---- src/server/video_service.rs | 8 ++++++++ src/ui.rs | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 443351469..8be0c6db5 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -60,7 +60,7 @@ extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_ return false; } *numModes = CFArrayGetCount(allModes); - for (int i = 0; i < *numModes && i < max; i++) { + for (uint32_t 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); @@ -136,7 +136,6 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h 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) && diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 025274840..b663b0f41 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -612,18 +612,18 @@ pub fn resolutions(name: &str) -> Vec { unsafe { if YES == MacGetModeNum(display, &mut num) { let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]); - let mut realNum = 0; + let mut real_num = 0; if YES == MacGetModes( display, widths.as_mut_ptr(), heights.as_mut_ptr(), num, - &mut realNum, + &mut real_num, ) { - if realNum <= num { - for i in 0..realNum { + if real_num <= num { + for i in 0..real_num { let resolution = Resolution { width: widths[i as usize] as _, height: heights[i as usize] as _, diff --git a/src/server/video_service.rs b/src/server/video_service.rs index a9a9fd9ab..affb5eb17 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -577,6 +577,14 @@ fn run(sp: GenericService) -> ResultType<()> { if last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; + // Capturer on macos does not return Err event the solution is changed. + #[cfg(target_os = "macos")] + if check_display_changed(c.ndisplay, c.current, c.width, c.height) { + log::info!("Displays changed"); + *SWITCH.lock().unwrap() = true; + bail!("SWITCH"); + } + if let Some(msg_out) = check_displays_changed() { sp.send(msg_out); } diff --git a/src/ui.rs b/src/ui.rs index a197cb257..e0449fd95 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, LocalConfig, PeerConfig}, + config::{LocalConfig, PeerConfig}, log, }; From 59cd775d5fe7606891ebbcff80f329510b52bff8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 25 Feb 2023 22:47:22 +0800 Subject: [PATCH 649/734] fix notify peer resolution change Signed-off-by: fufesou --- flutter/lib/models/model.dart | 79 +++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index eae41679f..e48d74dac 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -156,7 +156,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt, peerId); + updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel .receive(ChatModel.clientModeID, evt['text'] ?? ''); @@ -241,36 +241,33 @@ class FfiModel with ChangeNotifier { } } - handleSwitchDisplay(Map evt, String peerId) { - final oldOrientation = _display.width > _display.height; - var old = _pi.currentDisplay; - _pi.currentDisplay = int.parse(evt['display']); - _display.x = double.parse(evt['x']); - _display.y = double.parse(evt['y']); - _display.width = int.parse(evt['width']); - _display.height = int.parse(evt['height']); - _display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; - if (old != _pi.currentDisplay) { - parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); + _updateCurDisplay(String peerId, Display newDisplay) { + if (newDisplay != _display) { + if (newDisplay.x != _display.x || newDisplay.y != _display.y) { + parent.target?.cursorModel + .updateDisplayOrigin(newDisplay.x, newDisplay.y); + } + _display = newDisplay; + _updateSessionWidthHeight(peerId); } + } - _updateSessionWidthHeight(peerId, display.width, display.height); + handleSwitchDisplay(Map evt, String peerId) { + _pi.currentDisplay = int.parse(evt['display']); + var newDisplay = Display(); + newDisplay.x = double.parse(evt['x']); + newDisplay.y = double.parse(evt['y']); + newDisplay.width = int.parse(evt['width']); + newDisplay.height = int.parse(evt['height']); + newDisplay.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; + + _updateCurDisplay(peerId, newDisplay); try { CurrentDisplayState.find(peerId).value = _pi.currentDisplay; } catch (e) { // } - - // remote is mobile, and orientation changed - if ((_display.width > _display.height) != oldOrientation) { - gFFI.canvasModel.updateViewStyle(); - } - if (_pi.platform == kPeerPlatformLinux || - _pi.platform == kPeerPlatformWindows || - _pi.platform == kPeerPlatformMacOS) { - parent.target?.canvasModel.updateViewStyle(); - } parent.target?.recordingModel.onSwitchDisplay(); handleResolutions(peerId, evt["resolutions"]); notifyListeners(); @@ -372,7 +369,8 @@ class FfiModel with ChangeNotifier { }); } - _updateSessionWidthHeight(String id, int width, int height) { + _updateSessionWidthHeight(String id) { + parent.target?.canvasModel.updateViewStyle(); bind.sessionSetSize(id: id, width: display.width, height: display.height); } @@ -429,7 +427,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); + _updateSessionWidthHeight(peerId); } if (displays.isNotEmpty) { parent.target?.dialogManager.showLoading( @@ -488,7 +486,7 @@ 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]; + _updateCurDisplay(peerId, _pi.displays[_pi.currentDisplay]); } } notifyListeners(); @@ -797,12 +795,18 @@ class CanvasModel with ChangeNotifier { final dh = getDisplayHeight() * _scale; var dxOffset = 0; var dyOffset = 0; - if (dw > size.width) { - dxOffset = (x - dw * (x / size.width) - _x).toInt(); - } - if (dh > size.height) { - dyOffset = (y - dh * (y / size.height) - _y).toInt(); + try { + if (dw > size.width) { + dxOffset = (x - dw * (x / size.width) - _x).toInt(); + } + if (dh > size.height) { + dyOffset = (y - dh * (y / size.height) - _y).toInt(); + } + } catch (e) { + // Unhandled Exception: Unsupported operation: Infinity or NaN toInt + return; } + _x += dxOffset; _y += dyOffset; if (dxOffset != 0 || dyOffset != 0) { @@ -1579,6 +1583,19 @@ class Display { ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; } + + @override + bool operator ==(Object other) => + other is Display && + other.runtimeType == runtimeType && + _innerEqual(other); + + bool _innerEqual(Display other) => + other.x == x && + other.y == y && + other.width == width && + other.height == height && + other.cursorEmbedded == cursorEmbedded; } class Resolution { From e876adaec58ed45761a0aae250425d49b75faebd Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 25 Feb 2023 22:59:59 +0800 Subject: [PATCH 650/734] fix, flutter keyboard grab, connecting peers of older version Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 37bbbd66a..8500bec03 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -23,6 +23,10 @@ import '../../common/shared_state.dart'; import './popup_menu.dart'; import './kb_layout_type_chooser.dart'; +const _kKeyLegacyMode = 'legacy'; +const _kKeyMapMode = 'map'; +const _kKeyTranslateMode = 'translate'; + class MenubarState { final kStoreKey = 'remoteMenubarState'; late RxBool show; @@ -1540,9 +1544,10 @@ class _KeyboardMenu extends StatelessWidget { 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'); + if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) { + bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode); + } return Offstage(); } return _IconSubmenuButton( @@ -1555,13 +1560,13 @@ class _KeyboardMenu extends StatelessWidget { mode() { return futureBuilder(future: () async { - return await bind.sessionGetKeyboardMode(id: id) ?? 'legacy'; + return await bind.sessionGetKeyboardMode(id: id) ?? _kKeyLegacyMode; }(), 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'), + KeyboardModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'), + KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'), + KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), ]; List<_RadioMenuButton> list = []; onChanged(String? value) async { @@ -1571,13 +1576,13 @@ class _KeyboardMenu extends StatelessWidget { for (KeyboardModeMenu mode in modes) { if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) { - if (mode.key == 'translate') { + if (mode.key == _kKeyTranslateMode) { if (Platform.isLinux || pi.platform == kPeerPlatformLinux) { continue; } } var text = translate(mode.menu); - if (mode.key == 'translate') { + if (mode.key == _kKeyTranslateMode) { text = '$text beta'; } list.add(_RadioMenuButton( From 6740587d8adf6a8cc62c6d09ead26a1a878400c2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 25 Feb 2023 23:11:28 +0800 Subject: [PATCH 651/734] do not check keyboard permission on menu build Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 8500bec03..c27546d9f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1542,11 +1542,15 @@ class _KeyboardMenu extends StatelessWidget { @override Widget build(BuildContext context) { - var ffiModel = Provider.of(context); - if (ffiModel.permissions['keyboard'] == false) return Offstage(); + // Do not check permission here? + // var ffiModel = Provider.of(context); + // if (ffiModel.permissions['keyboard'] == false) return Offstage(); if (stateGlobal.grabKeyboard) { if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) { bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode); + } else if (bind.sessionIsKeyboardModeSupported( + id: id, mode: _kKeyLegacyMode)) { + bind.sessionSetKeyboardMode(id: id, value: _kKeyLegacyMode); } return Offstage(); } From 037641624c82aa5cd0cf6ae8475bb9ef82b4172a Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Sat, 25 Feb 2023 21:39:21 +0100 Subject: [PATCH 652/734] 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 d7e43b6bf..5ae368581 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -454,7 +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", ""), + ("Codec", "Códec"), + ("Resolution", "Resolución"), ].iter().cloned().collect(); } From 8092f3ad94f11112d8c37b60d45dfd928be1eeb1 Mon Sep 17 00:00:00 2001 From: PCKUO Date: Sun, 26 Feb 2023 12:51:16 +0800 Subject: [PATCH 653/734] Make up for the missing --- src/lang/tw.rs | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ca1193eaa..e20c7f254 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -37,19 +37,19 @@ 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", "關於"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "隱私聲明"), ("Mute", "靜音"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "建構日期"), + ("Version", "版本"), + ("Home", "主頁"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), ("Hardware Codec", "硬件編解碼"), @@ -213,15 +213,15 @@ 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", ""), + ("Connect via relay", "中繼連線"), ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "驗證"), + ("Remember me", "記住我"), + ("Trust this device", "信任此設備"), + ("Verification code", "驗證碼"), + ("verification_tip", "檢測到新設備登錄,已向註冊郵箱發送了登入驗證碼,請輸入驗證碼繼續登錄"), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), @@ -391,12 +391,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 發行版。 請嘗試 X11 桌面或更改您的操作系統。"), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), + ("Show RustDesk", "顯示 RustDesk"), + ("This PC", "此電腦"), + ("or", "或"), + ("Continue with", "使用"), ("Elevate", "提權"), - ("Zoom cursor", ""), + ("Zoom cursor", "縮放游標"), ("Accept sessions via password", "只允許密碼訪問"), ("Accept sessions via click", "只允許點擊訪問"), ("Accept sessions via both", "允許密碼或點擊訪問"), @@ -407,9 +407,9 @@ 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_experiment_tip", "Wayland 支持處於實驗階段,如果你需要使用無人值守訪問,請使用 X11。"), ("Right click to select tabs", "右鍵選擇選項卡"), - ("Skipped", ""), + ("Skipped", "已略過"), ("Add to Address Book", "添加到地址簿"), ("Group", "小組"), ("Search", "搜索"), @@ -418,8 +418,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "請選擇本地鍵盤類型"), ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), - ("config_input", ""), - ("config_microphone", ""), + ("config_input", "為了能夠通過鍵盤控制遠程桌面, 請給予 RustDesk \"輸入監控\" 權限。"), + ("config_microphone", "為了支持通過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), ("Wait", "等待"), ("Elevation Error", "提權失敗"), @@ -438,8 +438,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "弱"), ("Medium", "中"), ("Strong", "強"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "反轉訪問方向"), + ("Please confirm if you want to share your desktop?", "請確認是否要讓對方訪問你的桌面?"), ("Display", "顯示"), ("Default View Style", "默認顯示方式"), ("Default Scroll Style", "默認滾動方式"), From 4b2529125567207721489caaf2085532d998705c Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 26 Feb 2023 11:23:43 +0800 Subject: [PATCH 654/734] sciter/mobile id suffix "\r" or "/r" for relay Signed-off-by: 21pages --- flutter/lib/common.dart | 6 ++++-- .../lib/desktop/pages/connection_page.dart | 5 +---- src/flutter_ffi.rs | 4 ++++ src/ui.rs | 19 ++++++++++++++----- src/ui/index.tis | 5 ++++- src/ui/remote.rs | 3 ++- src/ui_interface.rs | 9 +++++++++ 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ff373cc9c..e89671711 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1453,10 +1453,12 @@ connectMainDesktop(String id, connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, - bool isRDP = false, - bool forceRelay = false}) async { + bool isRDP = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); + final oldId = id; + id = await bind.mainHandleRelayId(id: id); + final forceRelay = id != oldId; assert(!(isFileTransfer && isTcpTunneling && isRDP), "more than one connect type"); diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 4aad66eee..edbd5b7c6 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -151,10 +151,7 @@ class _ConnectionPageState extends State /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { 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); + connect(context, id, isFileTransfer: isFileTransfer); } /// UI for the remote ID TextField. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0866ff739..e49ba65f7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -840,6 +840,10 @@ pub fn main_get_user_default_option(key: String) -> SyncReturn { SyncReturn(get_user_default_option(key)) } +pub fn main_handle_relay_id(id: String) -> String { + handle_relay_id(id) +} + pub fn session_add_port_forward( id: String, local_port: i32, diff --git a/src/ui.rs b/src/ui.rs index e0449fd95..22c44ec5f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -420,8 +420,8 @@ impl UI { crate::lan::send_wol(id) } - fn new_remote(&mut self, id: String, remote_type: String) { - new_remote(id, remote_type) + fn new_remote(&mut self, id: String, remote_type: String, force_relay: bool) { + new_remote(id, remote_type, force_relay) } fn is_process_trusted(&mut self, _prompt: bool) -> bool { @@ -571,6 +571,10 @@ impl UI { fn default_video_save_directory(&self) -> String { default_video_save_directory() } + + fn handle_relay_id(&self, id: String) -> String { + handle_relay_id(id) + } } impl sciter::EventHandler for UI { @@ -588,7 +592,7 @@ impl sciter::EventHandler for UI { fn set_remote_id(String); fn closing(i32, i32, i32, i32); fn get_size(); - fn new_remote(String, bool); + fn new_remote(String, String, bool); fn send_wol(String); fn remove_peer(String); fn remove_discovered(String); @@ -653,6 +657,7 @@ impl sciter::EventHandler for UI { fn has_hwcodec(); fn get_langs(); fn default_video_save_directory(); + fn handle_relay_id(String); } } @@ -718,9 +723,13 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc> { } #[inline] -pub fn new_remote(id: String, remote_type: String) { +pub fn new_remote(id: String, remote_type: String, force_relay: bool) { let mut lock = CHILDREN.lock().unwrap(); - let args = vec![format!("--{}", remote_type), id.clone()]; + let mut args = vec![format!("--{}", remote_type), id.clone()]; + if force_relay { + args.push("".to_string()); // password + args.push("--relay".to_string()); + } let key = (id.clone(), remote_type.clone()); if let Some(c) = lock.1.get_mut(&key) { if let Ok(Some(_)) = c.try_wait() { diff --git a/src/ui/index.tis b/src/ui/index.tis index ec2e0a748..0e2247070 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -62,12 +62,15 @@ function createNewConnect(id, type) { id = id.replace(/\s/g, ""); app.remote_id.value = formatId(id); if (!id) return; + var old_id = id; + id = handler.handle_relay_id(id); + var force_relay = old_id != id; if (id == my_id) { msgbox("custom-error", "Error", "You cannot connect to your own computer"); return; } handler.set_remote_id(id); - handler.new_remote(id, type); + handler.new_remote(id, type, force_relay); } class ShareRdp: Reactor.Component { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index c6e0229b2..ed16f1e0e 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -462,6 +462,7 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { + let force_relay = args.contains(&"--relay".to_string()); let session: Session = Session { id: id.clone(), password: password.clone(), @@ -486,7 +487,7 @@ impl SciterSession { .lc .write() .unwrap() - .initialize(id, conn_type, None, false); + .initialize(id, conn_type, None, force_relay); Self(session) } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 3b2ba0897..62eba25c1 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -970,3 +970,12 @@ async fn check_id( } "" } + +// if it's relay id, return id processed, otherwise return original id +pub fn handle_relay_id(id: String) -> String { + if id.ends_with(r"\r") || id.ends_with(r"/r") { + id[0..id.len() - 2].to_string() + } else { + id + } +} From 8fa487e5b07f1c53b6cfd79fbda952e22e2bbcea Mon Sep 17 00:00:00 2001 From: Michal Witek Date: Sun, 26 Feb 2023 09:07:00 +0100 Subject: [PATCH 655/734] Added missing translation --- src/lang/pl.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index eea46accb..fe6454fad 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -284,13 +284,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Akceptujesz?"), ("Open System Setting", "Otwórz ustawienia systemowe"), ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"), - ("android_input_permission_tip1", "android_input_permission_tip1"), - ("android_input_permission_tip2", "android_input_permission_tip2"), - ("android_new_connection_tip", "android_new_connection_tip"), - ("android_service_will_start_tip", "android_service_will_start_tip"), - ("android_stop_service_tip", "android_stop_service_tip"), - ("android_version_audio_tip", "android_version_audio_tip"), - ("android_start_service_tip", "android_start_service_tip"), + ("android_input_permission_tip1", "Aby można było sterować Twoim urządzeniem za pomocą myszy lub dotyku, musisz zezwolić RustDesk na korzystanie z usługi \"Ułatwienia dostępu\"."), + ("android_input_permission_tip2", "Przejdź do następnej strony ustawień systemowych, znajdź i wejdź w [Zainstalowane usługi], włącz usługę [RustDesk Input]."), + ("android_new_connection_tip", "Otrzymano nowe żądanie zdalnego dostępu, które chce przejąć kontrolę nad Twoim urządzeniem."), + ("android_service_will_start_tip", "Włączenie opcji „Przechwytywanie ekranu” spowoduje automatyczne uruchomienie usługi, umożliwiając innym urządzeniom żądanie połączenia z Twoim urządzeniem."), + ("android_stop_service_tip", "Zamknięcie usługi spowoduje automatyczne zamknięcie wszystkich nawiązanych połączeń."), + ("android_version_audio_tip", "Bieżąca wersja systemu Android nie obsługuje przechwytywania dźwięku, zaktualizuj system do wersji Android 10 lub nowszej."), + ("android_start_service_tip", "Kliknij [Uruchom usługę] lub Otwórz [Przechwytywanie ekranu], aby uruchomić usługę udostępniania ekranu."), ("Account", "Konto"), ("Overwrite", "Nadpisz"), ("This file exists, skip or overwrite this file?", "Ten plik istnieje, pominąć czy nadpisać ten plik?"), @@ -311,7 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Język"), ("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"), ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"), - ("android_open_battery_optimizations_tip", "android_open_battery_optimizations_tip"), + ("android_open_battery_optimizations_tip", "Jeśli chcesz wyłączyć tę funkcję, przejdź do następnej strony ustawień aplikacji RustDesk, znajdź i wprowadź [Bateria], odznacz [Bez ograniczeń]"), ("Connection not allowed", "Połączenie niedozwolone"), ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), ("Map mode", "Tryb mapowania"), @@ -449,12 +449,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), + ("Voice call", "Rozmowa głosowa"), + ("Text chat", "Chat tekstowy"), + ("Stop voice call", "Rozłącz"), + ("relay_hint_tip", "Bezpośrednie połączenie może nie być możliwe, możesz spróbować połączyć się przez serwer przekazujący. \nDodatkowo, jeśli chcesz użyć serwera przekazującego przy pierwszej próbie, możesz dodać sufiks \"/r\" do identyfikatora lub wybrać opcję \"Zawsze łącz przez serwer przekazujący\" na karcie peer-ów."), + ("Reconnect", "Połącz ponownie"), + ("Codec", "Kodek"), + ("Resolution", "Rozdzielczość"), + ("Use temporary password", "Użyj hasła tymczasowego"), + ("Set temporary password length", "Ustaw długość hasła tymczasowego"), + ("Key", "Klucz") ].iter().cloned().collect(); } From ee893ce744dd2d06020a80849f707c9902fa495e Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Sun, 26 Feb 2023 09:13:42 +0100 Subject: [PATCH 656/734] changed empty job list logic --- flutter/assets/transfer.svg | 2 + .../lib/desktop/pages/file_manager_page.dart | 293 ++++++++++-------- 2 files changed, 163 insertions(+), 132 deletions(-) create mode 100644 flutter/assets/transfer.svg diff --git a/flutter/assets/transfer.svg b/flutter/assets/transfer.svg new file mode 100644 index 000000000..24149bf58 --- /dev/null +++ b/flutter/assets/transfer.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 5df9c9a80..c3322fef7 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -153,17 +153,9 @@ class _FileManagerPageState extends State backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Row( children: [ - Flexible( - flex: 3, - child: body(isLocal: true), - ), - Flexible( - flex: 3, - child: body(isLocal: false), - ), - model.jobTable.isEmpty - ? SizedBox() - : Flexible(flex: 2, child: statusList()) + Flexible(flex: 3, child: body(isLocal: true)), + Flexible(flex: 3, child: body(isLocal: false)), + Flexible(flex: 2, child: statusList()) ], ), ); @@ -550,6 +542,18 @@ class _FileManagerPageState extends State return false; } + Widget generateCard(Widget child) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: child, + ); + } + /// transfer status list /// watch transfer status Widget statusList() { @@ -558,140 +562,165 @@ class _FileManagerPageState extends State child: 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(15.0), - ), - ), + child: model.jobTable.isEmpty + ? generateCard( + Center( child: Column( - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Transform.rotate( - angle: item.isRemote ? pi : 0, - child: SvgPicture.asset( - "assets/arrow.svg", - color: Theme.of(context).tabBarTheme.labelColor, - ), - ).paddingOnly(left: 15), - const SizedBox( - width: 16.0, - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + SvgPicture.asset( + "assets/transfer.svg", + color: Theme.of(context).tabBarTheme.labelColor, + height: 40, + ).paddingOnly(bottom: 10), + Text( + translate("No transfers in progress"), + textAlign: TextAlign.center, + textScaleFactor: 1.20, + style: TextStyle( + color: Theme.of(context).tabBarTheme.labelColor), + ), + ], + ), + ), + ) + : Obx( + () => ListView.builder( + controller: ScrollController(), + itemBuilder: (BuildContext context, int index) { + final item = model.jobTable[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: generateCard( + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Tooltip( - waitDuration: Duration(milliseconds: 500), - message: item.jobName, - child: Text( - item.jobName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).paddingSymmetric(vertical: 10), + Transform.rotate( + angle: item.isRemote ? pi : 0, + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + ).paddingOnly(left: 15), + const SizedBox( + width: 16.0, ), - Text( - '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, + 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, + ).paddingSymmetric(vertical: 10), + ), + Text( + '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + ), + 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: 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), + ), + ], ), ), - Offstage( - offstage: item.state != JobState.inProgress, - child: Text( - '${translate("Speed")} ${readableFileSize(item.speed)}/s', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, + 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: Colors.white, + ), + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), ), - ), - ), - Offstage( - offstage: item.state == JobState.inProgress, - child: Text( - translate( - item.display(), + MenuButton( + padding: EdgeInsets.only(right: 15), + child: SvgPicture.asset( + "assets/close.svg", + color: Colors.white, + ), + onPressed: () { + model.jobTable.removeAt(index); + model.cancelJob(item.id); + }, + color: MyTheme.accent, + hoverColor: MyTheme.accent80, ), - 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), + ], ), ], ), - ), - 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: Colors.white, - ), - color: MyTheme.accent, - hoverColor: MyTheme.accent80, - ), - ), - MenuButton( - padding: EdgeInsets.only(right: 15), - child: SvgPicture.asset( - "assets/close.svg", - color: Colors.white, - ), - onPressed: () { - model.jobTable.removeAt(index); - model.cancelJob(item.id); - }, - color: MyTheme.accent, - hoverColor: MyTheme.button, - ), - ], - ), - ], + ], + ).paddingSymmetric(vertical: 10), ), - ], - ).paddingSymmetric(vertical: 10), + ); + }, + itemCount: model.jobTable.length, ), - ); - }, - itemCount: model.jobTable.length, - ), - ), + ), ), ); } From d8af4f2acb9b157b037203770ea445993425e71f Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Sun, 26 Feb 2023 09:14:22 +0100 Subject: [PATCH 657/734] added new string "No transfers in progress" --- 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/en.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, 34 insertions(+) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 45c552848..4a6536afa 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4824ac5e9..593e023d2 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "停止语音通话"), ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), ("Reconnect", "重连"), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e2761e45e..d09bba096 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 2020a2b6f..428150eff 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 7cf563fc3..8bfa973c4 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -454,5 +454,6 @@ 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"), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 4bfa86349..3e87cd661 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -44,5 +44,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), ("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."), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index c22532440..1ea05d5ab 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3ce2860f0..494dd4cb7 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -454,5 +454,6 @@ 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"), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 70051f3e8..72dd93a10 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), ("Reconnect", "اتصال مجدد"), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1f6e9f55b..a600c92eb 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index b7ebf4577..26e5a649c 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 21ab28214..125d982dc 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f48de17f6..17a9589a8 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 4c63106da..0749603b0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -454,5 +454,6 @@ 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"), + ("No transfers in progress", "Nessun trasferimento in corso"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b291a6e7a..7c810cbac 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d63e83187..87425b402 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b8b9eb1df..49017f4ff 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 1a806c803..615c3601b 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Stop spraakoproep"), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2b29c7cb2..ae1743ee2 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e91cd3909..4e619a6e4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b0fe9175d..8f4b2b13d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index d0232ba37..ebefd1a80 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index fe5d708ad..d652904cc 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), ("Reconnect", "Переподключить"), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 458002f4c..47843fd55 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2abd1870f..95d65a453 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6b739e8ab..3ec67d77a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 90a435fd7..31f408748 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index a98ea6346..3afcf1a9a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 61c2b5d28..53df7b4f7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 236ee5e8d..a5db9708c 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f2a34e212..96e35e2b1 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 84e74716f..f49e81a7b 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "停止語音聊天"), ("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"), ("Reconnect", "重連"), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 0c4caf4db..1729d42a7 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 19e1184d9..5521022b7 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -454,5 +454,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } From ab792f798e0581084de7bb965fe88b979b11925b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Sun, 26 Feb 2023 10:19:13 +0100 Subject: [PATCH 658/734] fix missing comma --- src/lang/pl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5269c4ee8..13027a682 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -458,7 +458,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resolution", "Rozdzielczość"), ("Use temporary password", "Użyj hasła tymczasowego"), ("Set temporary password length", "Ustaw długość hasła tymczasowego"), - ("Key", "Klucz") + ("Key", "Klucz"), ("No transfers in progress", ""), ].iter().cloned().collect(); } From b83583769b51a01071ea241704607647e5130872 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 26 Feb 2023 18:49:07 +0800 Subject: [PATCH 659/734] mount rustdesk rather than vdi/host --- Cargo.toml | 1 + vdi/host/.devcontainer/devcontainer.json | 5 +- vdi/host/Cargo.lock | 970 ++++++++++++++++++++++- vdi/host/Cargo.toml | 4 +- 4 files changed, 972 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f93f776a0..b53615c4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,7 @@ flutter_rust_bridge = "1.61.1" [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] +exclude = ["vdi/host"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." diff --git a/vdi/host/.devcontainer/devcontainer.json b/vdi/host/.devcontainer/devcontainer.json index 02c6e589b..f0016b5b1 100644 --- a/vdi/host/.devcontainer/devcontainer.json +++ b/vdi/host/.devcontainer/devcontainer.json @@ -4,8 +4,8 @@ "dockerfile": "./Dockerfile", "context": "." }, - "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", - "workspaceFolder": "/home/vscode/rustdesk", + "workspaceMount": "source=${localWorkspaceFolder}/../..,target=/home/vscode/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/vscode/rustdesk/vdi/host", "customizations": { "vscode": { "extensions": [ @@ -15,6 +15,7 @@ "tamasfe.even-better-toml", "serayuzgur.crates", "mhutchie.git-graph", + "formulahendry.terminal", "eamodio.gitlens" ], "settings": { diff --git a/vdi/host/Cargo.lock b/vdi/host/Cargo.lock index d4254717e..7b7cf26bd 100644 --- a/vdi/host/Cargo.lock +++ b/vdi/host/Cargo.lock @@ -2,6 +2,32 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -11,6 +37,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + [[package]] name = "async-broadcast" version = "0.3.4" @@ -73,7 +114,7 @@ dependencies = [ "parking", "polling", "slab", - "socket2", + "socket2 0.4.7", "waker-fn", "windows-sys 0.42.0", ] @@ -116,29 +157,73 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -146,6 +231,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.1.0" @@ -155,6 +265,57 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "confy" +version = "0.4.0" +source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" +dependencies = [ + "directories-next", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.7.1", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.14" @@ -164,6 +325,50 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cxx" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derivative" version = "2.2.0" @@ -175,6 +380,16 @@ dependencies = [ "syn", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "4.0.0" @@ -184,6 +399,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -195,12 +420,38 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "easy-parallel" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "enumflags2" version = "0.7.5" @@ -222,6 +473,19 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -237,6 +501,18 @@ dependencies = [ "instant", ] +[[package]] +name = "filetime" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.45.0", +] + [[package]] name = "futures" version = "0.3.26" @@ -349,14 +625,78 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hbb_common" +version = "0.1.0" +dependencies = [ + "anyhow", + "backtrace", + "bytes", + "chrono", + "confy", + "directories-next", + "dirs-next", + "env_logger", + "filetime", + "futures", + "futures-util", + "lazy_static", + "libc", + "log", + "mac_address", + "machine-uid", + "osascript", + "protobuf", + "protobuf-codegen", + "rand", + "regex", + "serde", + "serde_derive", + "socket2 0.3.19", + "sodiumoxide", + "sysinfo", + "tokio", + "tokio-socks", + "tokio-util", + "winapi", + "zstd", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] [[package]] name = "hex" @@ -364,6 +704,36 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -383,12 +753,54 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + [[package]] name = "libusb1-sys" version = "0.6.4" @@ -401,6 +813,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -420,6 +841,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mac_address" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" +dependencies = [ + "nix 0.23.2", + "winapi", +] + +[[package]] +name = "machine-uid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212" +dependencies = [ + "winreg", +] + [[package]] name = "memchr" version = "2.5.0" @@ -435,6 +875,49 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nix" version = "0.24.3" @@ -444,7 +927,7 @@ dependencies = [ "bitflags", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -456,6 +939,53 @@ dependencies = [ "memchr", ] +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "object" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -472,6 +1002,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "osascript" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "parking" version = "2.0.0" @@ -501,6 +1042,26 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -558,6 +1119,58 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "protobuf" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +dependencies = [ + "thiserror", +] + [[package]] name = "qemu-display" version = "0.1.0" @@ -588,6 +1201,7 @@ dependencies = [ name = "qemu-rustdesk" version = "0.1.0" dependencies = [ + "hbb_common", "qemu-display", ] @@ -630,6 +1244,28 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -686,12 +1322,39 @@ dependencies = [ "libusb1-sys", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + [[package]] name = "serde" version = "1.0.152" @@ -733,6 +1396,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.10" @@ -759,6 +1433,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + [[package]] name = "slab" version = "0.4.8" @@ -774,6 +1463,17 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.4.7" @@ -784,6 +1484,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519", + "libc", + "libsodium-sys", + "serde", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -801,6 +1513,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -815,6 +1542,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.38" @@ -835,6 +1571,91 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tokio" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.4.7", + "tokio-macros", + "windows-sys 0.42.0", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1-1" +source = "git+https://github.com/open-trade/tokio-socks#7034e79263ce25c348be072808d7601d82cd892d" +dependencies = [ + "bytes", + "either", + "futures-core", + "futures-sink", + "futures-util", + "pin-project", + "thiserror", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "hashbrown", + "pin-project-lite", + "slab", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.5.1" @@ -900,6 +1721,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "usbredirhost" version = "0.0.1" @@ -948,18 +1775,95 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "waker-fn" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + [[package]] name = "wepoll-ffi" version = "0.1.2" @@ -969,6 +1873,17 @@ dependencies = [ "cc", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" @@ -985,6 +1900,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1087,6 +2011,15 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi", +] + [[package]] name = "xml-rs" version = "0.8.4" @@ -1116,7 +2049,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.24.3", "once_cell", "ordered-stream", "rand", @@ -1157,6 +2090,35 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zstd" +version = "0.9.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.3+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "zvariant" version = "3.11.0" diff --git a/vdi/host/Cargo.toml b/vdi/host/Cargo.toml index 62c964122..6a67813a2 100644 --- a/vdi/host/Cargo.toml +++ b/vdi/host/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" authors = ["rustdesk "] edition = "2021" - [dependencies] -qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" } \ No newline at end of file +qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" } +hbb_common = { path = "../../libs/hbb_common" } From 459bed1a68cac7d1634e0101dd4fb05601be373b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Sun, 26 Feb 2023 11:49:22 +0100 Subject: [PATCH 660/734] dark theme fix --- flutter/lib/common.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e89671711..53a401230 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -189,7 +189,9 @@ class MyTheme { style: ButtonStyle(splashFactory: NoSplash.splashFactory), ) : null, - colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith( + colorScheme: ColorScheme.fromSwatch( + primarySwatch: Colors.blue, + ).copyWith( brightness: Brightness.light, background: Color(0xFFEEEEEE), ), @@ -229,9 +231,11 @@ class MyTheme { checkboxTheme: const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), colorScheme: ColorScheme.fromSwatch( - brightness: Brightness.dark, primarySwatch: Colors.blue, - ).copyWith(background: Color(0xFF24252B)), + ).copyWith( + brightness: Brightness.dark, + background: Color(0xFF24252B), + ), ).copyWith( extensions: >[ ColorThemeExtension.dark, From 0a52d64900a8f1f182073eb79affdee4cf6a596c Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 26 Feb 2023 23:29:36 +0800 Subject: [PATCH 661/734] print stack Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 23 ++++++++++++++++++----- flutter/lib/models/model.dart | 4 +++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index fca73eac7..37c4a39dc 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -503,8 +503,21 @@ class InputModel { } x += d.x; y += d.y; + var evtX = 0; + var evtY = 0; + try { + x.round(); + y.round(); + } catch (e) { + debugPrintStack( + label: 'canvasModel.scale value ${canvasModel.scale}, $e'); + return; + } - if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) { + if (evtX < d.x || + evtY < d.y || + evtX > (d.x + d.width) || + evtY > (d.y + d.height)) { // If left mouse up, no early return. if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { return; @@ -512,12 +525,12 @@ class InputModel { } if (type != '') { - x = 0; - y = 0; + evtX = 0; + evtY = 0; } - evt['x'] = '${x.round()}'; - evt['y'] = '${y.round()}'; + evt['x'] = '$evtX'; + evt['y'] = '$evtY'; var buttons = ''; switch (evt['buttons']) { case kPrimaryMouseButton: diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e48d74dac..74cc7f14f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -803,7 +803,9 @@ class CanvasModel with ChangeNotifier { dyOffset = (y - dh * (y / size.height) - _y).toInt(); } } catch (e) { - // Unhandled Exception: Unsupported operation: Infinity or NaN toInt + debugPrintStack( + label: + '(x,y) ($x,$y), (_x,_y) ($_x,$_y), _scale $_scale, display size (${getDisplayWidth()},${getDisplayHeight()}), size $size, , $e'); return; } From 544e39e11cf5127944bd8d112a453f74cfa7f801 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 26 Feb 2023 23:31:11 +0800 Subject: [PATCH 662/734] fix assign Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 37c4a39dc..b91453138 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -506,8 +506,8 @@ class InputModel { var evtX = 0; var evtY = 0; try { - x.round(); - y.round(); + evtX = x.round(); + evtY = y.round(); } catch (e) { debugPrintStack( label: 'canvasModel.scale value ${canvasModel.scale}, $e'); From 8073fa2386bb4a407203e4a383c2cd14b48b21f9 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 26 Feb 2023 19:43:24 +0100 Subject: [PATCH 663/734] Update de.rs --- src/lang/de.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index ee28fe0e7..3d95832ec 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -454,8 +454,8 @@ 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", ""), - ("No transfers in progress", ""), - ].iter().cloned().collect(); + ("Codec", "Codec"), + ("Resolution", "Auflösung"), + ("No transfers in progress", "Keine Übertragungen im Gange"), + ].iter().cloned().collect(); } From 5793c730c8f1f9c5c96786a656db6546435fceeb Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 26 Feb 2023 19:48:20 +0100 Subject: [PATCH 664/734] Update README-DE.md --- docs/README-DE.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index 8ee4a51fa..dd2aa8609 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -17,9 +17,9 @@ RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the b ![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. +RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [CONTRIBUTING-DE.md](CONTRIBUTING-DE.md) an, wenn du Unterstützung beim Start brauchst. -[**Wie arbeitet RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) @@ -41,6 +41,14 @@ Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann se | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM | | Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM | +## Dev-Container + +[![In Dev-Containern öffnen](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) + +Wenn du VS Code und Docker bereits installiert hast, kannst du auf das Abzeichen oben klicken, um loszulegen. Wenn du darauf klickst, wird VS Code automatisch die Dev-Container-Erweiterung installieren, den Quellcode in ein Container-Volume klonen und einen Dev-Container für die Verwendung aufsetzen. + +Weitere Informationen findest du in [DEVCONTAINER-DE.md](DEVCONTAINER-DE.md). + ## Abhängigkeiten Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter. From c580bc16cc86919157d0e327ca97bc26db27cea5 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 26 Feb 2023 19:48:55 +0100 Subject: [PATCH 665/734] Add files via upload --- docs/CONTRIBUTING-DE.md | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/CONTRIBUTING-DE.md diff --git a/docs/CONTRIBUTING-DE.md b/docs/CONTRIBUTING-DE.md new file mode 100644 index 000000000..6258a9a7a --- /dev/null +++ b/docs/CONTRIBUTING-DE.md @@ -0,0 +1,50 @@ +# Beitrge zu RustDesk + +RustDesk begrt Beitrge von jedem. Hier sind die Richtlinien, wenn Sie uns +helfen mchten: + +## Beitrge + +Beitrge zu RustDesk oder seinen Abhngigkeiten sollten in Form von Pull +Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur +(jemand mit der Erlaubnis, Korrekturen einzubringen) geprft und entweder in den +Hauptbaum eingefgt oder Feedback fr notwendige nderungen gegeben. Alle +Beitrge sollten diesem Format folgen, auch die von Hauptakteuren. + +Wenn Sie an einem Problem arbeiten mchten, melden Sie es bitte zuerst an, indem +Sie auf GitHub erklren, dass Sie daran arbeiten mchten. Damit soll verhindert +werden, dass Beitrge zum gleichen Thema doppelt bearbeitet werden. + +## Checkliste fr Pull Requests + +- Verzweigen Sie sich vom Master-Branch und, falls ntig, wechseln Sie zum + aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das + Zusammenfhren mit dem Master nicht reibungslos funktioniert, werden Sie + mglicherweise aufgefordert, Ihre nderungen zu berarbeiten. + +- Commits sollten so klein wie mglich sein und gleichzeitig sicherstellen, dass + jeder Commit unabhngig voneinander korrekt ist (d. h., jeder Commit sollte + sich bersetzen lassen und Tests bestehen). + +- Commits sollten von einem "Herkunftszertifikat fr Entwickler" + (https://developercertificate.org) begleitet werden, das besagt, dass Sie (und + ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE) + einverstanden sind. In Git ist dies die Option `-s` fr `git commit`. + +- Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur + Begutachtung bentigen, knnen Sie einem Gutachter mit @ antworten und um eine + Begutachtung des Pull Requests oder einen Kommentar bitten. Sie knnen auch + per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten. + +- Fgen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue + Funktion beziehen. + +Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Verhalten + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Kommunikation + +RustDesk-Mitarbeiter arbeiten hufig im [Discord](https://discord.gg/nDceKgxnkV). From d25c8df0501cf60ee2dd5e0699475f220fa9fea1 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 26 Feb 2023 19:49:36 +0100 Subject: [PATCH 666/734] Add files via upload --- docs/DEVCONTAINER-DE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docs/DEVCONTAINER-DE.md diff --git a/docs/DEVCONTAINER-DE.md b/docs/DEVCONTAINER-DE.md new file mode 100644 index 000000000..2a0d73f17 --- /dev/null +++ b/docs/DEVCONTAINER-DE.md @@ -0,0 +1,14 @@ + +Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Binrprogramm im Debug-Modus erstellt. + +Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an. + +Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgefhrt werden mssen, um bestimmte Builds zu erstellen. + +Kommando|Build-Typ|Modus +-|-|-| +`.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|release + From b9b4913ca0d5f402db9a7cb2422f7167603f08ce Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 25 Feb 2023 04:55:37 -0800 Subject: [PATCH 667/734] mac admin auth check --- src/platform/macos.mm | 30 ++++++++++++++++++++++++++++++ src/platform/macos.rs | 8 ++++++++ src/ui_interface.rs | 6 +++--- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 8be0c6db5..ac9a69d04 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -1,6 +1,9 @@ #import #import #import +#include +#include + // https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm @@ -35,6 +38,33 @@ extern "C" bool InputMonitoringAuthStatus(bool prompt) { return false; } +extern "C" bool MacCheckAdminAuthorization() { + AuthorizationRef authRef; + OSStatus status; + + status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults, &authRef); + if (status != errAuthorizationSuccess) { + printf("Failed to create AuthorizationRef\n"); + return false; + } + + AuthorizationItem authItem = {kAuthorizationRightExecute, 0, NULL, 0}; + AuthorizationRights authRights = {1, &authItem}; + AuthorizationFlags flags = kAuthorizationFlagDefaults | + kAuthorizationFlagInteractionAllowed | + kAuthorizationFlagPreAuthorize | + kAuthorizationFlagExtendRights; + status = AuthorizationCopyRights(authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL); + if (status != errAuthorizationSuccess) { + printf("Failed to authorize\n"); + return false; + } + + AuthorizationFree(authRef, kAuthorizationFlagDefaults); + return true; +} + extern "C" float BackingScaleFactor() { NSScreen* s = [NSScreen mainScreen]; if (s) return [s backingScaleFactor]; diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b663b0f41..5c4c68e2c 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -34,6 +34,7 @@ extern "C" { static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; + fn MacCheckAdminAuthorization() -> BOOL; fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL; fn MacGetModes( display: u32, @@ -665,3 +666,10 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType< } Ok(()) } + + +pub fn check_super_user_permission() -> ResultType { + unsafe { + Ok(MacCheckAdminAuthorization() == YES) + } +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 62eba25c1..471150f60 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -707,10 +707,10 @@ pub fn is_root() -> bool { pub fn check_super_user_permission() -> bool { #[cfg(feature = "flatpak")] return true; - #[cfg(any(windows, target_os = "linux"))] + #[cfg(any(windows, target_os = "linux", target_os = "macos"))] return crate::platform::check_super_user_permission().unwrap_or(false); - #[cfg(not(any(windows, target_os = "linux")))] - true + #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] + return true; } #[allow(dead_code)] From e3d704dfdee6712eca17964a74ac3bb1ebe97526 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 25 Feb 2023 21:26:09 +0800 Subject: [PATCH 668/734] ensure same bpp and framerate when get and set for mac --- src/platform/macos.mm | 83 +++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index ac9a69d04..3c90981c4 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -74,6 +74,33 @@ extern "C" float BackingScaleFactor() { // https://github.com/jhford/screenresolution/blob/master/cg_utils.c // https://github.com/jdoupe/screenres/blob/master/setgetscreen.m +size_t bitDepth(CGDisplayModeRef mode) { + size_t depth = 0; + // Deprecated, same display same bpp? + // https://stackoverflow.com/questions/8210824/how-to-avoid-cgdisplaymodecopypixelencoding-to-get-bpp + // https://github.com/libsdl-org/SDL/pull/6628 + 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; +} + extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); if (allModes == NULL) { @@ -85,16 +112,28 @@ extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { } 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) { + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { return false; } - *numModes = CFArrayGetCount(allModes); - for (uint32_t 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); + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return false; } + uint32_t allModeCount = CFArrayGetCount(allModes); + uint32_t realNum = 0; + for (uint32_t i = 0; i < allModeCount && realNum < max; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && + bitDepth(currentMode) == bitDepth(mode)) { + widths[realNum] = (uint32_t)CGDisplayModeGetWidth(mode); + heights[realNum] = (uint32_t)CGDisplayModeGetHeight(mode); + realNum++; + } + } + *numModes = realNum; + CGDisplayModeRelease(currentMode); CFRelease(allModes); return true; } @@ -110,31 +149,8 @@ extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t 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) { +static bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { CGError rc; CGDisplayConfigRef config; rc = CGBeginDisplayConfiguration(&config); @@ -152,7 +168,6 @@ bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { return true; } - extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height) { bool ret = false; @@ -170,8 +185,8 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); if (width == CGDisplayModeGetWidth(mode) && height == CGDisplayModeGetHeight(mode) && - bitDepth(currentMode) == bitDepth(mode) && - CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode)) { + CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && + bitDepth(currentMode) == bitDepth(mode)) { ret = setDisplayToMode(display, mode); break; } From b6c3c74286e74b79e1eb688d4f44035f70dbf6a1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 27 Feb 2023 12:01:22 +0800 Subject: [PATCH 669/734] opt: move the resize area out of the flutter view --- flutter/lib/consts.dart | 7 +++++++ flutter/lib/desktop/pages/port_forward_tab_page.dart | 12 +++++++----- flutter/lib/desktop/pages/remote_tab_page.dart | 2 ++ .../lib/desktop/screen/desktop_remote_screen.dart | 5 +++++ flutter/lib/desktop/widgets/tabbar_widget.dart | 7 +++++++ flutter/lib/models/state_model.dart | 11 +++++++++++ flutter/pubspec.yaml | 4 ++-- 7 files changed, 41 insertions(+), 7 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 537784918..789517bf7 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/state_model.dart'; const double kDesktopRemoteTabBarHeight = 28.0; const int kMainWindowId = 0; @@ -58,6 +59,11 @@ const double kDesktopFileTransferMaximumWidth = 300; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; +EdgeInsets get kDragToResizeAreaPadding => Platform.isLinux + ? stateGlobal.fullscreen || stateGlobal.maximize + ? EdgeInsets.zero + : EdgeInsets.all(4.0) + : EdgeInsets.zero; // https://en.wikipedia.org/wiki/Non-breaking_space const int $nbsp = 0x00A0; @@ -79,6 +85,7 @@ const kDefaultScrollAmountMultiplier = 5.0; const kDefaultScrollDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; +const kMaximizeEdgeSize = 0.0; var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0; const kWindowBorderWidth = 1.0; const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index f2d75d00f..394d89e38 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -109,11 +109,13 @@ class _PortForwardTabPageState extends State { ); return Platform.isMacOS ? tabWidget - : SubWindowDragToResizeArea( - child: tabWidget, - resizeEdgeSize: stateGlobal.resizeEdgeSize.value, - windowId: stateGlobal.windowId, - ); + : Obx( + () => SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + ), + ); } void onRemoveId(String id) { diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 0deb646c0..e7aed0358 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -210,6 +210,8 @@ class _ConnectionTabPageState extends State { : Obx(() => SubWindowDragToResizeArea( key: contentKey, child: tabWidget, + // Specially configured for a better resize area and remote control. + childPadding: kDragToResizeAreaPadding, resizeEdgeSize: stateGlobal.resizeEdgeSize.value, windowId: stateGlobal.windowId, )); diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index bb6bc431b..64af41401 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; @@ -26,6 +28,9 @@ class DesktopRemoteScreen extends StatelessWidget { ChangeNotifierProvider.value(value: gFFI.canvasModel), ], child: Scaffold( + // Set transparent background for padding the resize area out of the flutter view. + // This allows the wallpaper goes through our resize area. (Linux only now). + backgroundColor: Platform.isLinux ? Colors.transparent : null, body: ConnectionTabPage( params: params, ), diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index ee3aaaf2c..958c4c035 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -523,12 +523,18 @@ class WindowActionPanelState extends State super.dispose(); } + void _setMaximize(bool maximize) { + stateGlobal.setMaximize(maximize); + setState(() {}); + } + @override void onWindowMaximize() { // catch maximize from system if (!widget.isMaximized.value) { widget.isMaximized.value = true; } + _setMaximize(true); super.onWindowMaximize(); } @@ -538,6 +544,7 @@ class WindowActionPanelState extends State if (widget.isMaximized.value) { widget.isMaximized.value = false; } + _setMaximize(false); super.onWindowUnmaximize(); } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 761c95ded..7de258a39 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -9,8 +9,10 @@ import '../consts.dart'; class StateGlobal { int _windowId = -1; bool _fullscreen = false; + bool _maximize = false; bool grabKeyboard = false; final RxBool _showTabBar = true.obs; + final RxBool _showResizeEdge = true.obs; final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; @@ -18,12 +20,21 @@ class StateGlobal { int get windowId => _windowId; bool get fullscreen => _fullscreen; + bool get maximize => _maximize; double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight; RxBool get showTabBar => _showTabBar; RxDouble get resizeEdgeSize => _resizeEdgeSize; RxDouble get windowBorderWidth => _windowBorderWidth; setWindowId(int id) => _windowId = id; + setMaximize(bool v) { + if (_maximize != v) { + print("set maximize"); + _maximize = v; + _resizeEdgeSize.value = + _maximize ? kMaximizeEdgeSize : kWindowEdgeSize; + } + } setFullscreen(bool v) { if (_fullscreen != v) { _fullscreen = v; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 572b3e20a..b10dd1ec0 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: f37357ed98a10717576eb9ed8413e92b2ec5d13a + ref: c717159ea8fc0b9c6b4ac50110efc1dfd3c1ba7a freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.4 window_size: @@ -76,7 +76,7 @@ dependencies: 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. + # currently, we use flutter 3.7.0+. # # 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). From cb4b658e483c50c0028f0675eedff5bf40703a81 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 27 Feb 2023 14:24:15 +0800 Subject: [PATCH 670/734] Avoid dividing by 0 and setting scale to 0 Signed-off-by: fufesou --- flutter/lib/models/model.dart | 38 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 74cc7f14f..21949705f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -617,13 +617,28 @@ class ViewStyle { final int displayWidth; final int displayHeight; ViewStyle({ - this.style = '', - this.width = 0.0, - this.height = 0.0, - this.displayWidth = 0, - this.displayHeight = 0, + required this.style, + required this.width, + required this.height, + required this.displayWidth, + required this.displayHeight, }); + static defaultViewStyle() { + final desktop = (isDesktop || isWebDesktop); + final w = + desktop ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth; + final h = + desktop ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; + return ViewStyle( + style: '', + width: w.toDouble(), + height: h.toDouble(), + displayWidth: w, + displayHeight: h, + ); + } + static int _double2Int(double v) => (v * 100).round().toInt(); @override @@ -652,9 +667,14 @@ class ViewStyle { double get scale { double s = 1.0; if (style == kRemoteViewStyleAdaptive) { - final s1 = width / displayWidth; - final s2 = height / displayHeight; - s = s1 < s2 ? s1 : s2; + if (width != 0 && + height != 0 && + displayWidth != 0 && + displayHeight != 0) { + final s1 = width / displayWidth; + final s2 = height / displayHeight; + s = s1 < s2 ? s1 : s2; + } } return s; } @@ -680,7 +700,7 @@ class CanvasModel with ChangeNotifier { // scroll offset y percent double _scrollY = 0.0; ScrollStyle _scrollStyle = ScrollStyle.scrollauto; - ViewStyle _lastViewStyle = ViewStyle(); + ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle(); final _imageOverflow = false.obs; From c18c6d72bd6e5a914c7acea91154d48c69bc3b3b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 27 Feb 2023 09:44:52 +0100 Subject: [PATCH 671/734] create folder modern dialog --- flutter/lib/common.dart | 47 ++++++- .../lib/desktop/pages/file_manager_page.dart | 120 ++++++++++++------ 2 files changed, 119 insertions(+), 48 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 53a401230..a73ab8bd3 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -168,6 +168,22 @@ class MyTheme { brightness: Brightness.light, hoverColor: Color.fromARGB(255, 224, 224, 224), scaffoldBackgroundColor: Color(0xFFFFFFFF), + dialogBackgroundColor: Color(0xFFFFFFFF), + dialogTheme: DialogTheme( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + inputDecorationTheme: InputDecorationTheme( + fillColor: Color(0xFFEEEEEE), + filled: true, + isDense: true, + contentPadding: EdgeInsets.all(15), + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide.none, + ), + ), textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19, color: Colors.black87), titleSmall: TextStyle(fontSize: 14, color: Colors.black87), @@ -205,6 +221,22 @@ class MyTheme { brightness: Brightness.dark, hoverColor: Color.fromARGB(255, 45, 46, 53), scaffoldBackgroundColor: Color(0xFF18191E), + dialogBackgroundColor: Color(0xFF18191E), + dialogTheme: DialogTheme( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + inputDecorationTheme: InputDecorationTheme( + fillColor: Color(0xFF24252B), + filled: true, + isDense: true, + contentPadding: EdgeInsets.all(15), + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide.none, + ), + ), textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19), titleSmall: TextStyle(fontSize: 14), @@ -681,18 +713,19 @@ class CustomAlertDialog extends StatelessWidget { scrollable: true, title: title, titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), - contentPadding: EdgeInsets.fromLTRB(contentPadding ?? padding, 25, - contentPadding ?? padding, actions is List ? 10 : padding), + contentPadding: EdgeInsets.fromLTRB( + contentPadding ?? padding, + 25, + contentPadding ?? padding, + actions is List ? 10 : padding, + ), content: ConstrainedBox( constraints: contentBoxConstraints, - child: Theme( - data: Theme.of(context).copyWith( - inputDecorationTheme: InputDecorationTheme( - isDense: true, contentPadding: EdgeInsets.all(15))), - child: content), + child: content, ), actions: actions, actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding), + actionsAlignment: MainAxisAlignment.center, ), ); } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index badb68a84..31f9e154b 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -49,6 +49,11 @@ enum MouseFocusScope { none } +final buttonShape = + MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), +)); + class FileManagerPage extends StatefulWidget { const FileManagerPage({Key? key, required this.id, this.forceRelay}) : super(key: key); @@ -300,14 +305,13 @@ class _FileManagerPageState extends State } skipCount = index + 1; } - var searchResult = entries - .skip(skipCount) - .where((element) => element.name.toLowerCase().startsWith(buffer)); + var searchResult = entries.skip(skipCount).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.toLowerCase().startsWith(buffer)); + searchResult = entries.where( + (element) => element.name.toLowerCase().startsWith(buffer)); } if (searchResult.isEmpty) { setState(() { @@ -321,8 +325,8 @@ class _FileManagerPageState extends State onSearch: (buffer) { debugPrint("searching for $buffer"); final selectedEntries = getSelectedItems(isLocal); - final searchResult = - entries.where((element) => element.name.toLowerCase().startsWith(buffer)); + final searchResult = entries.where( + (element) => element.name.toLowerCase().startsWith(buffer)); selectedEntries.clear(); if (searchResult.isEmpty) { setState(() { @@ -504,8 +508,7 @@ class _FileManagerPageState extends State debugPrint("entry is not valid: ${entry.path}"); } final selectedEntries = getSelectedItems(isLocal); - final searchResult = - entries.where((element) => element == entry); + final searchResult = entries.where((element) => element == entry); selectedEntries.clear(); if (searchResult.isEmpty) { return; @@ -976,25 +979,66 @@ class _FileManagerPageState extends State cancel() => close(false); return CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - autofocus: true, + SvgPicture.asset("assets/folder_new.svg", + color: MyTheme.accent), + Text( + translate("Create Folder"), + ).paddingOnly( + left: 10, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name", + ), + ), + controller: name, + autofocus: true, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all( + MyTheme.accent, + ), + shape: buttonShape, + ), + icon: Icon(Icons.close_rounded), + label: Text(translate("Cancel")), + onPressed: cancel, + ), + ElevatedButton.icon( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all( + MyTheme.accent, + ), + shape: buttonShape, + ), + icon: Icon(Icons.done_rounded), + label: Text(translate("Ok")), + onPressed: submit, + ), + ], + ).paddingOnly(top: 20) + ], ), ], ), - actions: [ - dialogButton("Cancel", - onPressed: cancel, isOutline: true), - dialogButton("OK", onPressed: submit) - ], onSubmit: submit, onCancel: cancel, ); @@ -1036,11 +1080,7 @@ class _FileManagerPageState extends State ? MyTheme.accent80 : MyTheme.accent, ), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), - ), - ), + shape: buttonShape, ), onPressed: validItems(selectedItems) ? () { @@ -1430,14 +1470,14 @@ class _FileManagerPageState extends State ).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()) + flex: 1, + child: ascending.value != null + ? Icon( + ascending.value! + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + ) + : const Offstage()) ], ), ), @@ -1467,10 +1507,8 @@ class _FileManagerPageState extends State axis: Axis.vertical, onPointerMove: (dx) { nameColWidth.value += dx; - nameColWidth.value = min( - kDesktopFileTransferMaximumWidth, - max(kDesktopFileTransferMinimumWidth, - nameColWidth.value)); + nameColWidth.value = min(kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, nameColWidth.value)); }, padding: padding, ), From 6e6fc64f629c6b1a9067a7f16645451cd11f1073 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 27 Feb 2023 15:56:09 +0800 Subject: [PATCH 672/734] opt: add resize area into the compatible ui mode --- flutter/lib/consts.dart | 2 +- flutter/lib/desktop/pages/desktop_tab_page.dart | 2 +- flutter/lib/desktop/pages/file_manager_tab_page.dart | 2 +- flutter/lib/desktop/pages/port_forward_tab_page.dart | 2 +- flutter/lib/desktop/pages/remote_tab_page.dart | 2 +- flutter/lib/models/state_model.dart | 1 - flutter/pubspec.yaml | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 789517bf7..b43f3e7da 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -59,7 +59,7 @@ const double kDesktopFileTransferMaximumWidth = 300; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; -EdgeInsets get kDragToResizeAreaPadding => Platform.isLinux +EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && Platform.isLinux ? stateGlobal.fullscreen || stateGlobal.maximize ? EdgeInsets.zero : EdgeInsets.all(4.0) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 053a2d8a2..4a1a40242 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -75,7 +75,7 @@ class _DesktopTabPageState extends State { isClose: false, ), ))); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : Obx( () => DragToResizeArea( diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 148d928d9..39958e88e 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -98,7 +98,7 @@ class _FileManagerTabPageState extends State { labelGetter: DesktopTab.labelGetterAlias, )), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : SubWindowDragToResizeArea( child: tabWidget, diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 394d89e38..32f02c9b7 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -107,7 +107,7 @@ class _PortForwardTabPageState extends State { labelGetter: DesktopTab.labelGetterAlias, )), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : Obx( () => SubWindowDragToResizeArea( diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index e7aed0358..d810650fd 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -205,7 +205,7 @@ class _ConnectionTabPageState extends State { ), ), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : Obx(() => SubWindowDragToResizeArea( key: contentKey, diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 7de258a39..aa4fab86e 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -29,7 +29,6 @@ class StateGlobal { setWindowId(int id) => _windowId = id; setMaximize(bool v) { if (_maximize != v) { - print("set maximize"); _maximize = v; _resizeEdgeSize.value = _maximize ? kMaximizeEdgeSize : kWindowEdgeSize; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b10dd1ec0..8d390d370 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: c717159ea8fc0b9c6b4ac50110efc1dfd3c1ba7a + ref: e383fffb5c4529c9e0a710f1025a0c590b99ee08 freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.4 window_size: From 920fa6dac7c2d9aa6b709398a3f7cb10c65fcd7f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 27 Feb 2023 16:53:13 +0800 Subject: [PATCH 673/734] opt: resize padding set to 5.0 --- flutter/lib/consts.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index b43f3e7da..3c414cd85 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -62,7 +62,7 @@ const double kDesktopFileTransferHeaderHeight = 25.0; EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && Platform.isLinux ? stateGlobal.fullscreen || stateGlobal.maximize ? EdgeInsets.zero - : EdgeInsets.all(4.0) + : EdgeInsets.all(5.0) : EdgeInsets.zero; // https://en.wikipedia.org/wiki/Non-breaking_space const int $nbsp = 0x00A0; From 2f7e758c751538e3caf025081b230ed2b49ee60a Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Mon, 27 Feb 2023 09:56:56 +0100 Subject: [PATCH 674/734] Update nl.rs corrected error --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index eb7c214ab..f38c14791 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -444,7 +444,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default View Style", "Standaard Weergave Stijl"), ("Default Scroll Style", "Standaard Scroll Stijl"), ("Default Image Quality", "Standaard Beeldkwaliteit"), - ("Default Codec", "tandaard Codec"), + ("Default Codec", "Standaard Codec"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), ("Auto", "Auto"), From 6971f45fd0734b4e56d416d9ee0bba49eabb5334 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 27 Feb 2023 17:54:18 +0800 Subject: [PATCH 675/734] dark theme install page Signed-off-by: 21pages --- flutter/lib/desktop/pages/install_page.dart | 20 ++++++++++---------- flutter/lib/main.dart | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index e7bb28813..d7202e300 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -46,15 +46,19 @@ class _InstallPageState extends State with WindowListener { final double em = 13; final btnFontSize = 0.9 * em; final double button_radius = 6; + final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark; final buttonStyle = OutlinedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(button_radius)), )); final inputBorder = OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide(color: Colors.black12)); + borderSide: + BorderSide(color: isDarkTheme ? Colors.white70 : Colors.black12)); + final textColor = isDarkTheme ? null : Colors.black87; + final dividerColor = isDarkTheme ? Colors.white70 : Colors.black87; return Scaffold( - backgroundColor: Colors.white, + backgroundColor: null, body: SingleChildScrollView( child: Column( children: [ @@ -91,8 +95,7 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Change Path'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(left: em)) ], ).marginSymmetric(vertical: 2 * em), @@ -127,8 +130,7 @@ class _InstallPageState extends State with WindowListener { )).marginOnly(top: 2 * em), Row(children: [Text(translate('agreement_tip'))]) .marginOnly(top: em), - Divider(color: Colors.black87) - .marginSymmetric(vertical: 0.5 * em), + Divider(color: dividerColor).marginSymmetric(vertical: 0.5 * em), Row( children: [ Expanded( @@ -143,8 +145,7 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Cancel'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(right: 2 * em)), Obx(() => ElevatedButton( onPressed: btnEnabled.value ? install : null, @@ -167,8 +168,7 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Run without install'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(left: 2 * em)), ), ], diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c61287d4f..baf7193b3 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -291,7 +291,7 @@ void _runApp( void runInstallPage() async { await windowManager.ensureInitialized(); await initEnv(kAppTypeMain); - _runApp('', const InstallPage(), ThemeMode.light); + _runApp('', const InstallPage(), MyTheme.currentThemeMode()); windowManager.waitUntilReadyToShow( WindowOptions(size: Size(800, 600), center: true), () async { windowManager.show(); From 2678c503a01aaac28936a6128019c72e77e578ca Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 27 Feb 2023 20:28:55 +0800 Subject: [PATCH 676/734] chore: fix recursive os.system on my m1 in build.py, and modify windows subsystem macro --- build.py | 184 ++++++++++++++++++++++++---------------------------- src/main.rs | 7 +- 2 files changed, 90 insertions(+), 101 deletions(-) diff --git a/build.py b/build.py index 727b53fe0..b30f76f95 100755 --- a/build.py +++ b/build.py @@ -18,14 +18,11 @@ exe_path = 'target/release/' + hbb_name flutter_win_target_dir = 'flutter/build/windows/runner/Release/' skip_cargo = False -def custom_os_system(cmd): - err = os._system(cmd) +def system2(cmd): + err = os.system(cmd) if err != 0: print(f"Error occurred when executing: {cmd}. Exiting.") sys.exit(-1) -# replace prebuilt os.system -os._system = os.system -os.system = custom_os_system def get_version(): with open("Cargo.toml", encoding="utf-8") as fh: @@ -144,8 +141,8 @@ def generate_build_script_for_docker(): # build rustdesk ./build.py --flutter --hwcodec ''') - os.system("chmod +x /tmp/build.sh") - os.system("bash /tmp/build.sh") + system2("chmod +x /tmp/build.sh") + system2("bash /tmp/build.sh") def download_extract_features(features, res_dir): @@ -250,7 +247,7 @@ def get_features(args): def generate_control_file(version): control_file_path = "../res/DEBIAN/control" - os.system('/bin/rm -rf %s' % control_file_path) + system2('/bin/rm -rf %s' % control_file_path) content = """Package: rustdesk Version: %s @@ -268,45 +265,45 @@ Description: A remote control software. def ffi_bindgen_function_refactor(): # workaround ffigen - os.system( + system2( 'sed -i "s/ffi.NativeFunction> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") - os.system('mkdir -p tmpdeb/DEBIAN') + system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) - os.system('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') + system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - os.system('dpkg-deb -b tmpdeb rustdesk.deb;') + system2('dpkg-deb -b tmpdeb rustdesk.deb;') - os.system('/bin/rm -rf tmpdeb/') - os.system('/bin/rm -rf ../res/DEBIAN/control') + system2('/bin/rm -rf tmpdeb/') + system2('/bin/rm -rf ../res/DEBIAN/control') os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.chdir("..") @@ -314,16 +311,16 @@ def build_flutter_deb(version, features): def build_flutter_dmg(version, features): if not skip_cargo: # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project - os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') + system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') # copy dylib - os.system( + system2( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") # ffi_bindgen_function_refactor() # limitations from flutter rust bridge - os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') + system2('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( + system2('flutter build macos --release') + system2( "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.chdir("..") @@ -331,29 +328,29 @@ def build_flutter_dmg(version, features): def build_flutter_arch_manjaro(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + system2(f'cargo build --features {features} --lib --release') ffi_bindgen_function_refactor() os.chdir('flutter') - os.system('flutter build linux --release') - os.system('strip build/linux/x64/release/bundle/lib/librustdesk.so') + system2('flutter build linux --release') + system2('strip build/linux/x64/release/bundle/lib/librustdesk.so') os.chdir('../res') - os.system('HBB=`pwd`/.. FLUTTER=1 makepkg -f') + system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f') def build_flutter_windows(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + system2(f'cargo build --features {features} --lib --release') if not os.path.exists("target/release/librustdesk.dll"): print("cargo build failed, please check rust source code.") exit(-1) os.chdir('flutter') - os.system('flutter build windows --release') + system2('flutter build windows --release') os.chdir('..') shutil.copy2('target/release/deps/dylib_virtual_display.dll', flutter_win_target_dir) os.chdir('libs/portable') - os.system('pip3 install -r requirements.txt') - os.system( + system2('pip3 install -r requirements.txt') + system2( f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') os.chdir('../..') if os.path.exists('./rustdesk_portable.exe'): @@ -374,22 +371,15 @@ def main(): parser = make_parser() args = parser.parse_args() - shutil.copy2('Cargo.toml', 'Cargo.toml.bk') - shutil.copy2('src/main.rs', 'src/main.rs.bk') - if windows: - txt = open('src/main.rs', encoding='utf8').read() - with open('src/main.rs', 'wt', encoding='utf8') as fh: - fh.write(txt.replace( - '//#![windows_subsystem', '#![windows_subsystem')) if os.path.exists(exe_path): os.unlink(exe_path) if os.path.isfile('/usr/bin/pacman'): - os.system('git checkout src/ui/common.tis') + system2('git checkout src/ui/common.tis') version = get_version() features = ','.join(get_features(args)) flutter = args.flutter if not flutter: - os.system('python3 res/inline-sciter.py') + system2('python3 res/inline-sciter.py') print(args.skip_cargo) if args.skip_cargo: skip_cargo = True @@ -397,55 +387,55 @@ def main(): if windows: # build virtual display dynamic library os.chdir('libs/virtual_display/dylib') - os.system('cargo build --release') + system2('cargo build --release') os.chdir('../../..') if flutter: build_flutter_windows(version, features) return - os.system('cargo build --release --features ' + features) - # os.system('upx.exe target/release/rustdesk.exe') - os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe') + system2('cargo build --release --features ' + features) + # system2('upx.exe target/release/rustdesk.exe') + system2('mv target/release/rustdesk.exe target/release/RustDesk.exe') pa = os.environ.get('P') if pa: - os.system( + system2( f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' 'target\\release\\rustdesk.exe') else: print('Not signed') - os.system( + system2( f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) + system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) if flutter: build_flutter_arch_manjaro(version, features) else: - os.system('cargo build --release --features ' + features) - os.system('git checkout src/ui/common.tis') - os.system('strip target/release/rustdesk') - os.system('ln -s res/pacman_install && ln -s res/PKGBUILD') - os.system('HBB=`pwd` makepkg -f') - os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( + system2('cargo build --release --features ' + features) + system2('git checkout src/ui/common.tis') + system2('strip target/release/rustdesk') + system2('ln -s res/pacman_install && ln -s res/PKGBUILD') + system2('HBB=`pwd` makepkg -f') + system2('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( version, version)) # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): - os.system('cargo build --release --features ' + features) - os.system('strip target/release/rustdesk') - os.system( + system2('cargo build --release --features ' + features) + system2('strip target/release/rustdesk') + system2( "sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec') - os.system( + system2('HBB=`pwd` rpmbuild -ba res/rpm.spec') + system2( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( version, version)) # yum localinstall rustdesk.rpm elif os.path.isfile('/usr/bin/zypper'): - os.system('cargo build --release --features ' + features) - os.system('strip target/release/rustdesk') - os.system( + system2('cargo build --release --features ' + features) + system2('strip target/release/rustdesk') + system2( "sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') - os.system( + system2('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') + system2( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % ( version, version)) # yum localinstall rustdesk.rpm @@ -455,18 +445,18 @@ def main(): build_flutter_dmg(version, features) pass else: - # os.system( + # system2( # 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') build_flutter_deb(version, features) else: - os.system('cargo bundle --release --features ' + features) + system2('cargo bundle --release --features ' + features) if osx: - os.system( + system2( 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') - os.system( + system2( 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') # https://github.com/sindresorhus/create-dmg - os.system('/bin/rm -rf *.dmg') + system2('/bin/rm -rf *.dmg') plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" txt = open(plist).read() with open(plist, "wt") as fh: @@ -476,7 +466,7 @@ def main(): """)) pa = os.environ.get('P') if pa: - os.system(''' + system2(''' # buggy: rcodesign sign ... path/*, have to sign one by one # install rcodesign via cargo install apple-codesign #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk @@ -486,11 +476,11 @@ def main(): codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app '''.format(pa)) - os.system('create-dmg target/release/bundle/osx/RustDesk.app') + system2('create-dmg target/release/bundle/osx/RustDesk.app') os.rename('RustDesk %s.dmg' % version, 'rustdesk-%s.dmg' % version) if pa: - os.system(''' + system2(''' # https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html # https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html # https://developer.apple.com/developer-id/ @@ -507,34 +497,32 @@ def main(): print('Not signed') else: # buid deb package - os.system( + system2( 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') - os.system('dpkg-deb -R rustdesk.deb tmpdeb') - os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( + system2('dpkg-deb -R rustdesk.deb tmpdeb') + system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + system2( 'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( + system2( 'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') - os.system( + system2( 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') - os.system( + system2( 'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') - os.system('strip tmpdeb/usr/bin/rustdesk') - os.system('mkdir -p tmpdeb/usr/lib/rustdesk') - os.system('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') - os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') + system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') + system2('strip tmpdeb/usr/bin/rustdesk') + system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') + system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/lib/rustdesk/libsciter-gtk.so') - os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) - os.system("mv Cargo.toml.bk Cargo.toml") - os.system("mv src/main.rs.bk src/main.rs") def md5_file(fn): md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest() - os.system('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) + system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) if __name__ == "__main__": diff --git a/src/main.rs b/src/main.rs index 169515425..3759f6056 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -// Specify the Windows subsystem to eliminate console window. -// Requires Rust 1.18. -//#![windows_subsystem = "windows"] + #![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" + )] use librustdesk::*; From f94791793bdec5d83b011709261857a6d6a8f653 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 27 Feb 2023 19:22:52 +0800 Subject: [PATCH 677/734] fix can't install when username contains &, @, ^ Signed-off-by: 21pages --- src/platform/windows.rs | 25 +++++++++++++++++++++++++ src/server/portable_service.rs | 12 +----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6b3f8013c..0bba649f4 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1236,6 +1236,15 @@ pub fn uninstall_me() -> ResultType<()> { fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType { let mut tmp = std::env::temp_dir(); + // When dir contains these characters, the bat file will not execute in elevated mode. + if vec!["&", "@", "^"] + .drain(..) + .any(|s| tmp.to_string_lossy().to_string().contains(s)) + { + if let Ok(dir) = user_accessible_folder() { + tmp = dir; + } + } tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext)); let mut file = std::fs::File::create(&tmp)?; // in case cmds mixed with \r\n and \n, make sure all ending with \r\n @@ -1872,3 +1881,19 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType< Ok(()) } } + +pub fn user_accessible_folder() -> ResultType { + let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); + let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); + // NOTICE: "C:\Windows\Temp" requires permanent authorization. + let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); + let dir; + if dir1.exists() { + dir = dir1; + } else if dir2.exists() { + dir = dir2; + } else { + bail!("no vaild user accessible folder"); + } + Ok(dir) +} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 7514ead38..c49f974a7 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -117,17 +117,7 @@ impl SharedMemory { } fn flink(name: String) -> ResultType { - let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); - let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); - let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); - let mut dir; - if dir1.exists() { - dir = dir1; - } else if dir2.exists() { - dir = dir2; - } else { - bail!("no vaild flink directory"); - } + let mut dir = crate::platform::user_accessible_folder()?; dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); if !dir.exists() { std::fs::create_dir(&dir)?; From c7e8037a79f3446591e585bbdfe52ed696c26873 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 27 Feb 2023 15:57:46 +0300 Subject: [PATCH 678/734] Update ru.rs --- src/lang/ru.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 07b8af998..3bfb5357d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -454,8 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), ("Reconnect", "Переподключить"), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), + ("Codec", "Кодек"), + ("Resolution", "Разрешение"), + ("No transfers in progress", "Передача не осуществляется"), ].iter().cloned().collect(); } From 63185a5bcb6272ad42b3f5abbaa54a9724fc03e7 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 27 Feb 2023 23:07:52 +0900 Subject: [PATCH 679/734] 1. enable BootReceiver. 2. add PermissionRequestTransparentActivity. 3. opt const. --- .../android/app/src/main/AndroidManifest.xml | 21 +++++--- .../com/carriez/flutter_hbb/BootReceiver.kt | 17 ++++-- .../com/carriez/flutter_hbb/MainActivity.kt | 51 ++++-------------- .../com/carriez/flutter_hbb/MainService.kt | 37 ++++++++----- .../PermissionRequestTransparentActivity.kt | 54 +++++++++++++++++++ .../kotlin/com/carriez/flutter_hbb/common.kt | 21 ++++++-- .../app/src/main/res/values/styles.xml | 8 +++ 7 files changed, 137 insertions(+), 72 deletions(-) create mode 100644 flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index ede6353ef..6f646b913 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -11,22 +11,24 @@ - + + android:supportsRtl="true"> + android:enabled="true" + android:exported="true"> + + @@ -53,8 +55,6 @@ android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize"> - - @@ -62,6 +62,11 @@ + + - + \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt index 328701567..a49dcc326 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt @@ -4,18 +4,25 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build +import android.util.Log import android.widget.Toast +const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED" + class BootReceiver : BroadcastReceiver() { + private val logTag = "tagBootReceiver" + override fun onReceive(context: Context, intent: Intent) { - if ("android.intent.action.BOOT_COMPLETED" == intent.action){ - val it = Intent(context,MainService::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + Log.d(logTag, "onReceive ${intent.action}") + + if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) { + val it = Intent(context, MainService::class.java).apply { + action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE } - Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show(); + Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(it) - }else{ + } else { context.startService(it) } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index fd340f7ed..73dbd2dad 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -7,12 +7,10 @@ package com.carriez.flutter_hbb * Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG */ -import android.app.Activity import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection -import android.media.projection.MediaProjectionManager import android.os.Build import android.os.IBinder import android.provider.Settings @@ -23,7 +21,6 @@ import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel -const val MEDIA_REQUEST_CODE = 42 class MainActivity : FlutterActivity() { companion object { @@ -32,7 +29,6 @@ class MainActivity : FlutterActivity() { private val channelTag = "mChannel" private val logTag = "mMainActivity" - private var mediaProjectionResultIntent: Intent? = null private var mainService: MainService? = null @RequiresApi(Build.VERSION_CODES.M) @@ -58,7 +54,7 @@ class MainActivity : FlutterActivity() { result.success(false) return@setMethodCallHandler } - getMediaProjection() + requestMediaProjection() result.success(true) } "start_capture" -> { @@ -153,35 +149,6 @@ class MainActivity : FlutterActivity() { } } - private fun getMediaProjection() { - val mMediaProjectionManager = - getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager - val mIntent = mMediaProjectionManager.createScreenCaptureIntent() - startActivityForResult(mIntent, MEDIA_REQUEST_CODE) - } - - private fun initService() { - if (mediaProjectionResultIntent == null) { - Log.w(logTag, "initService fail,mediaProjectionResultIntent is null") - return - } - Log.d(logTag, "Init service") - val serviceIntent = Intent(this, MainService::class.java) - serviceIntent.action = INIT_SERVICE - serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent) - - launchMainService(serviceIntent) - } - - private fun launchMainService(intent: Intent) { - // TEST api < O - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } - } - private fun initInput() { val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) if (intent.resolveActivity(packageManager) != null) { @@ -200,15 +167,17 @@ class MainActivity : FlutterActivity() { } } + private fun requestMediaProjection() { + val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply { + action = ACT_REQUEST_MEDIA_PROJECTION + } + startActivityForResult(intent, REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION) + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == MEDIA_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK && data != null) { - mediaProjectionResultIntent = data - initService() - } else { - flutterMethodChannel.invokeMethod("on_media_projection_canceled", null) - } + if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) { + flutterMethodChannel.invokeMethod("on_media_projection_canceled", null) } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index cf8e12e92..e28311964 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -43,10 +43,6 @@ import java.nio.ByteBuffer import kotlin.math.max import kotlin.math.min -const val EXTRA_MP_DATA = "mp_intent" -const val INIT_SERVICE = "init_service" -const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY" -const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY" const val DEFAULT_NOTIFY_TITLE = "RustDesk" const val DEFAULT_NOTIFY_TEXT = "Service is running" @@ -195,6 +191,7 @@ class MainService : Service() { override fun onCreate() { super.onCreate() + Log.d(logTag,"MainService onCreate") HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply { start() serviceLooper = looper @@ -203,6 +200,7 @@ class MainService : Service() { updateScreenInfo(resources.configuration.orientation) initNotification() startServer() + createForegroundNotification() } override fun onDestroy() { @@ -277,22 +275,25 @@ class MainService : Service() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d("whichService", "this service:${Thread.currentThread()}") + Log.d("whichService", "this service: ${Thread.currentThread()}") super.onStartCommand(intent, flags, startId) - if (intent?.action == INIT_SERVICE) { - Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}") - createForegroundNotification() - val mMediaProjectionManager = + if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) { + Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") + val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager - intent.getParcelableExtra(EXTRA_MP_DATA)?.let { + + intent.getParcelableExtra(EXT_MEDIA_PROJECTION_RES_INTENT)?.let { mediaProjection = - mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) + mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) checkMediaPermission() init(this) _isReady = true + } ?: let { + Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection") + requestMediaProjection() } } - return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control + return START_NOT_STICKY // don't use sticky (auto restart), the new service (from auto restart) will lose control } override fun onConfigurationChanged(newConfig: Configuration) { @@ -300,6 +301,14 @@ class MainService : Service() { updateScreenInfo(newConfig.orientation) } + private fun requestMediaProjection() { + val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply { + action = ACT_REQUEST_MEDIA_PROJECTION + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + } + @SuppressLint("WrongConstant") private fun createSurface(): Surface? { return if (useVP9) { @@ -653,8 +662,8 @@ class MainService : Service() { @SuppressLint("UnspecifiedImmutableFlag") private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent { val intent = Intent(this, MainService::class.java).apply { - action = ACTION_LOGIN_REQ_NOTIFY - putExtra(EXTRA_LOGIN_REQ_NOTIFY, res) + action = ACT_LOGIN_REQ_NOTIFY + putExtra(EXT_LOGIN_REQ_NOTIFY, res) } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt new file mode 100644 index 000000000..3beb7ec6b --- /dev/null +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt @@ -0,0 +1,54 @@ +package com.carriez.flutter_hbb + +import android.app.Activity +import android.content.Intent +import android.media.projection.MediaProjectionManager +import android.os.Build +import android.os.Bundle +import android.util.Log + +class PermissionRequestTransparentActivity: Activity() { + private val logTag = "permissionRequest" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(logTag, "onCreate PermissionRequestTransparentActivity: intent.action: ${intent.action}") + + when (intent.action) { + ACT_REQUEST_MEDIA_PROJECTION -> { + val mediaProjectionManager = + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + val intent = mediaProjectionManager.createScreenCaptureIntent() + startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION) + } + else -> finish() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQ_REQUEST_MEDIA_PROJECTION) { + if (resultCode == RESULT_OK && data != null) { + launchService(data) + } else { + setResult(RES_FAILED) + } + } + + finish() + } + + private fun launchService(mediaProjectionResultIntent: Intent) { + Log.d(logTag, "Launch MainService") + val serviceIntent = Intent(this, MainService::class.java) + serviceIntent.action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE + serviceIntent.putExtra(EXT_MEDIA_PROJECTION_RES_INTENT, mediaProjectionResultIntent) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } else { + startService(serviceIntent) + } + } + +} \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index 4bf244a06..a812686ec 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -12,8 +12,7 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.os.PowerManager -import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS -import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +import android.provider.Settings.* import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat.getSystemService import com.hjq.permissions.Permission @@ -22,6 +21,21 @@ import java.nio.ByteBuffer import java.util.* +// intent action, extra +const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION" +const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE" +const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" +const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT" +const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" + +// Activity requestCode +const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101 +const val REQ_REQUEST_MEDIA_PROJECTION = 201 + +// Activity responseCode +const val RES_FAILED = -100 + + @SuppressLint("ConstantLocale") val LOCAL_NAME = Locale.getDefault().toString() val SCREEN_INFO = Info(0, 0, 1, 200) @@ -59,9 +73,8 @@ fun requestPermission(context: Context, type: String) { } "application_details_settings" -> { try { - context.startActivity(Intent().apply { + context.startActivity(Intent(ACTION_APPLICATION_DETAILS_SETTINGS).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - action = "android.settings.APPLICATION_DETAILS_SETTINGS" data = Uri.parse("package:" + context.packageName) }) } catch (e:Exception) { diff --git a/flutter/android/app/src/main/res/values/styles.xml b/flutter/android/app/src/main/res/values/styles.xml index d74aa35c2..146267c91 100644 --- a/flutter/android/app/src/main/res/values/styles.xml +++ b/flutter/android/app/src/main/res/values/styles.xml @@ -15,4 +15,12 @@ + From 658c45d1c233a7be5997e38db701ea2af9c81c45 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 27 Feb 2023 21:46:26 +0800 Subject: [PATCH 680/734] Do not use flutter texture for render. 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 b08193971..ffcadd18b 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,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features hwcodec,flutter,${{ 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,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,${{ 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,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release ;; esac From fb21f9df440101739875eece1c45383272394cd1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 27 Feb 2023 22:24:00 +0800 Subject: [PATCH 681/734] check divide by 0 Signed-off-by: fufesou --- flutter/lib/models/model.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 21949705f..fd97b1e5c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -810,6 +810,10 @@ class CanvasModel with ChangeNotifier { double get tabBarHeight => stateGlobal.tabBarHeight; moveDesktopMouse(double x, double y) { + if (size.width == 0 || size.height == 0) { + return; + } + // On mobile platforms, move the canvas with the cursor. final dw = getDisplayWidth() * _scale; final dh = getDisplayHeight() * _scale; From 8cd9f8745d99686598347d1f0cd0d0192d1ec288 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 28 Feb 2023 00:41:09 +0900 Subject: [PATCH 682/734] opt AndroidPermissionManager --- .../com/carriez/flutter_hbb/MainActivity.kt | 8 ++ .../kotlin/com/carriez/flutter_hbb/common.kt | 84 +++++-------------- flutter/lib/common.dart | 27 ++---- flutter/lib/consts.dart | 26 ++++-- flutter/lib/mobile/pages/server_page.dart | 12 +-- flutter/lib/mobile/pages/settings_page.dart | 10 ++- flutter/lib/models/server_model.dart | 19 +++-- 7 files changed, 85 insertions(+), 101 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 73dbd2dad..7050e7a46 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -88,6 +88,14 @@ class MainActivity : FlutterActivity() { result.success(false) } } + START_ACTION -> { + if (call.arguments is String) { + startAction(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } "check_video_permission" -> { mainService?.let { result.success(it.checkMediaPermission()) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index a812686ec..0bc6c1c28 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -1,5 +1,6 @@ package com.carriez.flutter_hbb +import android.Manifest.permission.* import android.annotation.SuppressLint import android.content.Context import android.content.Intent @@ -35,6 +36,10 @@ const val REQ_REQUEST_MEDIA_PROJECTION = 201 // Activity responseCode const val RES_FAILED = -100 +// Flutter channel +const val START_ACTION = "start_action"; +const val IGNORE_BATTERY_OPTIMIZATIONS = "ignore_battery_optimizations"; + @SuppressLint("ConstantLocale") val LOCAL_NAME = Locale.getDefault().toString() @@ -44,56 +49,10 @@ data class Info( var width: Int, var height: Int, var scale: Int, var dpi: Int ) -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -fun testVP9Support(): Boolean { - return true - val res = MediaCodecList(MediaCodecList.ALL_CODECS) - .findEncoderForFormat( - MediaFormat.createVideoFormat( - MediaFormat.MIMETYPE_VIDEO_VP9, - SCREEN_INFO.width, - SCREEN_INFO.width - ) - ) - return res != null -} - @RequiresApi(Build.VERSION_CODES.M) fun requestPermission(context: Context, type: String) { - val permission = when (type) { - "ignore_battery_optimizations" -> { - try { - context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { - data = Uri.parse("package:" + context.packageName) - }) - } catch (e:Exception) { - e.printStackTrace() - } - return - } - "application_details_settings" -> { - try { - context.startActivity(Intent(ACTION_APPLICATION_DETAILS_SETTINGS).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - data = Uri.parse("package:" + context.packageName) - }) - } catch (e:Exception) { - e.printStackTrace() - } - return - } - "audio" -> { - Permission.RECORD_AUDIO - } - "file" -> { - Permission.MANAGE_EXTERNAL_STORAGE - } - else -> { - return - } - } XXPermissions.with(context) - .permission(permission) + .permission(type) .request { _, all -> if (all) { Handler(Looper.getMainLooper()).post { @@ -108,22 +67,23 @@ fun requestPermission(context: Context, type: String) { @RequiresApi(Build.VERSION_CODES.M) fun checkPermission(context: Context, type: String): Boolean { - val permission = when (type) { - "ignore_battery_optimizations" -> { - val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager - return pw.isIgnoringBatteryOptimizations(context.packageName) - } - "audio" -> { - Permission.RECORD_AUDIO - } - "file" -> { - Permission.MANAGE_EXTERNAL_STORAGE - } - else -> { - return false - } + if (IGNORE_BATTERY_OPTIMIZATIONS == type) { + val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager + return pw.isIgnoringBatteryOptimizations(context.packageName) + } + return XXPermissions.isGranted(context, type) +} + +@RequiresApi(Build.VERSION_CODES.M) +fun startAction(context: Context, action: String) { + try { + context.startActivity(Intent(action).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + data = Uri.parse("package:" + context.packageName) + }) + } catch (e: Exception) { + e.printStackTrace() } - return XXPermissions.isGranted(context, permission) } class AudioReader(val bufSize: Int, private val maxFrames: Int) { diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 53a401230..41ac595f2 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -907,21 +907,14 @@ class AccessibilityListener extends StatelessWidget { } } -class PermissionManager { +class AndroidPermissionManager { static Completer? _completer; static Timer? _timer; static var _current = ""; - static final permissions = [ - "audio", - "file", - "ignore_battery_optimizations", - "application_details_settings" - ]; - static bool isWaitingFile() { if (_completer != null) { - return !_completer!.isCompleted && _current == "file"; + return !_completer!.isCompleted && _current == kManageExternalStorage; } return false; } @@ -930,9 +923,6 @@ class PermissionManager { if (isDesktop) { return Future.value(true); } - if (!permissions.contains(type)) { - return Future.error("Wrong permission!$type"); - } return gFFI.invokeMethod("check_permission", type); } @@ -940,17 +930,16 @@ class PermissionManager { if (isDesktop) { return Future.value(true); } - if (!permissions.contains(type)) { - return Future.error("Wrong permission!$type"); - } gFFI.invokeMethod("request_permission", type); - if (type == "ignore_battery_optimizations") { + + // kIgnoreBatteryOptimizations permission doesn't depend on callback result, the result will be checked and updated on page resume + if (type == kIgnoreBatteryOptimizations) { return Future.value(false); } + _current = type; _completer = Completer(); - gFFI.invokeMethod("request_permission", type); // timeout _timer?.cancel(); @@ -1484,8 +1473,8 @@ connect(BuildContext context, String id, } } else { if (isFileTransfer) { - if (!await PermissionManager.check("file")) { - if (!await PermissionManager.request("file")) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { + if (!await AndroidPermissionManager.request(kManageExternalStorage)) { return; } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 3c414cd85..a0766a874 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -59,11 +59,12 @@ const double kDesktopFileTransferMaximumWidth = 300; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; -EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && Platform.isLinux - ? stateGlobal.fullscreen || stateGlobal.maximize - ? EdgeInsets.zero - : EdgeInsets.all(5.0) - : EdgeInsets.zero; +EdgeInsets get kDragToResizeAreaPadding => + !kUseCompatibleUiMode && Platform.isLinux + ? stateGlobal.fullscreen || stateGlobal.maximize + ? EdgeInsets.zero + : EdgeInsets.all(5.0) + : EdgeInsets.zero; // https://en.wikipedia.org/wiki/Non-breaking_space const int $nbsp = 0x00A0; @@ -136,6 +137,21 @@ const kRemoteAudioDualWay = 'dual-way'; const kIgnoreDpi = true; +/// Android constants +const kActionApplicationDetailsSettings = + "android.settings.APPLICATION_DETAILS_SETTINGS"; +const kActionRequestIgnoreBatteryOptimizations = + "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; +const kRecordAudio = "android.permission.RECORD_AUDIO"; +const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE"; +const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW"; + +/// [kIgnoreBatteryOptimizations] not a Android Permission, it is a custom key, used in `ignore battery optimizations` check +const kIgnoreBatteryOptimizations = "ignore_battery_optimizations"; + +/// Android channel invoke type key +const kStartAction = "start_action"; + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index abccdf683..648448f41 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; +import '../../consts.dart'; import '../../models/platform_model.dart'; import '../../models/server_model.dart'; import 'home_page.dart'; @@ -150,10 +151,11 @@ class _ServerPageState extends State { } void checkService() async { - gFFI.invokeMethod("check_service"); // jvm - // for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page - if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { - PermissionManager.complete("file", await PermissionManager.check("file")); + gFFI.invokeMethod("check_service"); + // for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page + if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { + AndroidPermissionManager.complete(kManageExternalStorage, + await AndroidPermissionManager.check(kManageExternalStorage)); debugPrint("file permission finished"); } } @@ -567,7 +569,7 @@ void androidChannelInit() { { var type = arguments["type"] as String; var result = arguments["result"] as bool; - PermissionManager.complete(type, result); + AndroidPermissionManager.complete(type, result); break; } case "on_media_projection_canceled": diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index c5f3b6935..6bdb7e813 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; import '../../common/widgets/login.dart'; +import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -133,7 +134,8 @@ class _SettingsState extends State with WidgetsBindingObserver { } Future updateIgnoreBatteryStatus() async { - final res = await PermissionManager.check("ignore_battery_optimizations"); + final res = + await AndroidPermissionManager.check(kIgnoreBatteryOptimizations); if (_ignoreBatteryOpt != res) { _ignoreBatteryOpt = res; return true; @@ -265,7 +267,8 @@ class _SettingsState extends State with WidgetsBindingObserver { ]), onToggle: (v) async { if (v) { - PermissionManager.request("ignore_battery_optimizations"); + gFFI.invokeMethod( + kStartAction, kActionRequestIgnoreBatteryOptimizations); } else { final res = await gFFI.dialogManager .show((setState, close) => CustomAlertDialog( @@ -282,7 +285,8 @@ class _SettingsState extends State with WidgetsBindingObserver { ], )); if (res == true) { - PermissionManager.request("application_details_settings"); + gFFI.invokeMethod( + kStartAction, kActionApplicationDetailsSettings); } } })); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index b2043f3c2..433990a2a 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; @@ -154,7 +155,8 @@ class ServerModel with ChangeNotifier { /// file true by default (if permission on) checkAndroidPermission() async { // audio - if (androidVersion < 30 || !await PermissionManager.check("audio")) { + if (androidVersion < 30 || + !await AndroidPermissionManager.check(kRecordAudio)) { _audioOk = false; bind.mainSetOption(key: "enable-audio", value: "N"); } else { @@ -163,7 +165,7 @@ class ServerModel with ChangeNotifier { } // file - if (!await PermissionManager.check("file")) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { _fileOk = false; bind.mainSetOption(key: "enable-file-transfer", value: "N"); } else { @@ -229,8 +231,8 @@ class ServerModel with ChangeNotifier { } toggleAudio() async { - if (!_audioOk && !await PermissionManager.check("audio")) { - final res = await PermissionManager.request("audio"); + if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) { + final res = await AndroidPermissionManager.request(kRecordAudio); if (!res) { // TODO handle fail return; @@ -243,8 +245,10 @@ class ServerModel with ChangeNotifier { } toggleFile() async { - if (!_fileOk && !await PermissionManager.check("file")) { - final res = await PermissionManager.request("file"); + if (!_fileOk && + !await AndroidPermissionManager.check(kManageExternalStorage)) { + final res = + await AndroidPermissionManager.request(kManageExternalStorage); if (!res) { // TODO handle fail return; @@ -561,7 +565,8 @@ class ServerModel with ChangeNotifier { } Future closeAll() async { - await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id))); + await Future.wait( + _clients.map((client) => bind.cmCloseConnection(connId: client.id))); _clients.clear(); tabController.state.value.tabs.clear(); } From 7bbeb351ce4eb6c582a7b162b4e415270b86b1be Mon Sep 17 00:00:00 2001 From: Michal Witek Date: Mon, 27 Feb 2023 20:40:39 +0100 Subject: [PATCH 683/734] Updated Flutter Version to `3.7.5` Updated agents OS to `Ubuntu 20.04` where possible --- .github/workflows/flutter-ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 74e4efa99..cae5b82c7 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -18,7 +18,7 @@ on: env: LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.7.0" + FLUTTER_VERSION: "3.7.5" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -260,7 +260,7 @@ jobs: job: - { target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-args: "", } steps: @@ -330,13 +330,13 @@ jobs: - { arch: x86_64, target: aarch64-linux-android, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "", } # - { # arch: x86_64, # target: armv7-linux-androideabi, - # os: ubuntu-18.04, + # os: ubuntu-20.04, # extra-build-features: "", # } steps: @@ -907,19 +907,19 @@ jobs: - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "", } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "flatpak", } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "appimage", } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } From 828c201fe092cc3ac33d61a70288f07b3345ed53 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 27 Feb 2023 20:56:45 +0100 Subject: [PATCH 684/734] modern file manager delete dialog --- flutter/lib/common.dart | 77 ++++++++++++++-- .../lib/desktop/pages/file_manager_page.dart | 14 --- flutter/lib/models/file_model.dart | 87 +++++++++++-------- 3 files changed, 125 insertions(+), 53 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a73ab8bd3..3680b0a11 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -202,9 +202,43 @@ class MyTheme { splashFactory: isDesktop ? NoSplash.splashFactory : null, textButtonTheme: isDesktop ? TextButtonThemeData( - style: ButtonStyle(splashFactory: NoSplash.splashFactory), + style: ButtonStyle( + splashFactory: NoSplash.splashFactory, + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), ) : null, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + MyTheme.accent, + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), + ), + checkboxTheme: const CheckboxThemeData( + splashRadius: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ), + listTileTheme: ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ), colorScheme: ColorScheme.fromSwatch( primarySwatch: Colors.blue, ).copyWith( @@ -257,11 +291,44 @@ class MyTheme { OutlinedButton.styleFrom(side: BorderSide(color: Colors.white38))), textButtonTheme: isDesktop ? TextButtonThemeData( - style: ButtonStyle(splashFactory: NoSplash.splashFactory), + style: ButtonStyle( + splashFactory: NoSplash.splashFactory, + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), ) : null, - checkboxTheme: - const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + MyTheme.accent, + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), + ), + checkboxTheme: const CheckboxThemeData( + checkColor: MaterialStatePropertyAll(dark), + splashRadius: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ), + listTileTheme: ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ), colorScheme: ColorScheme.fromSwatch( primarySwatch: Colors.blue, ).copyWith( @@ -684,7 +751,7 @@ class CustomAlertDialog extends StatelessWidget { Future.delayed(Duration.zero, () { if (!scopeNode.hasFocus) scopeNode.requestFocus(); }); - const double padding = 16; + const double padding = 30; bool tabTapped = false; return FocusScope( node: scopeNode, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 31f9e154b..f276961b0 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1010,25 +1010,11 @@ class _FileManagerPageState extends State MainAxisAlignment.spaceBetween, children: [ ElevatedButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all( - MyTheme.accent, - ), - shape: buttonShape, - ), icon: Icon(Icons.close_rounded), label: Text(translate("Cancel")), onPressed: cancel, ), ElevatedButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all( - MyTheme.accent, - ), - shape: buttonShape, - ), icon: Icon(Icons.done_rounded), label: Text(translate("Ok")), onPressed: submit, diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 5817e54fe..7e702f6f2 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -593,9 +593,11 @@ class FileModel extends ChangeNotifier { ? "${translate("Are you sure you want to delete the file of this directory?")}\n" : ""; final count = entries.length > 1 ? "${i + 1}/${entries.length}" : ""; - content = "$dirShow$count \n${entries[i].path}"; - final confirm = - await showRemoveDialog(title, content, item.isDirectory); + content = "$dirShow\n\n${entries[i].path}".trim(); + final confirm = await showRemoveDialog( + count.isEmpty ? title : "$title ($count)", + content, + item.isDirectory); try { if (confirm == true) { sendRemoveFile(entries[i].path, i, items.isLocal!); @@ -636,42 +638,59 @@ class FileModel extends ChangeNotifier { submit() => close(true); return CustomAlertDialog( title: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.warning, color: Colors.red), - const SizedBox(width: 20), - Text(title) + const Icon(Icons.warning_rounded, color: Colors.red), + Text(title).paddingOnly( + left: 10, + ), ], ), contentBoxConstraints: - BoxConstraints(minHeight: 100, minWidth: 400, maxWidth: 400), + BoxConstraints(minHeight: 80, minWidth: 400, maxWidth: 400), content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(content), - const SizedBox(height: 5), - Text(translate("This is irreversible!"), - style: const TextStyle(fontWeight: FontWeight.bold)), - showCheckbox - ? CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate("Do this for all conflicts"), - ), - value: removeCheckboxRemember, - onChanged: (v) { - if (v == null) return; - setState(() => removeCheckboxRemember = v); - }, - ) - : const SizedBox.shrink() - ]), - actions: [ - dialogButton("Cancel", onPressed: cancel, isOutline: true), - dialogButton("OK", onPressed: submit), - ], + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(content), + Text( + translate("This is irreversible!"), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ).paddingOnly(top: 20), + showCheckbox + ? CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Do this for all conflicts"), + ), + value: removeCheckboxRemember, + onChanged: (v) { + if (v == null) return; + setState(() => removeCheckboxRemember = v); + }, + ) + : const SizedBox.shrink(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + icon: Icon(Icons.close_rounded), + label: Text(translate("Cancel")), + onPressed: cancel, + ), + ElevatedButton.icon( + icon: Icon(Icons.done_rounded), + label: Text(translate("Ok")), + onPressed: submit, + ), + ], + ).paddingOnly(top: 20) + ], + ), onSubmit: submit, onCancel: cancel, ); From 0380fc0c9e6235b86af42d99041a55401ea63e13 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Tue, 28 Feb 2023 00:50:07 +0100 Subject: [PATCH 685/734] Update es.rs New term added --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index af0da0479..a95d39776 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -456,6 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reconnect", "Reconectar"), ("Codec", "Códec"), ("Resolution", "Resolución"), - ("No transfers in progress", ""), + ("No transfers in progress", "No hay transferencias en curso"), ].iter().cloned().collect(); } From 48100c9e91ed7ac725e8651f5fbc458c4a71613c Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 28 Feb 2023 11:31:30 +0900 Subject: [PATCH 686/734] 1. add _systemAlertWindow and _enableStartOnBoot options. 2. opt settings_page.dart state variables --- .../android/app/src/main/AndroidManifest.xml | 1 + .../com/carriez/flutter_hbb/MainActivity.kt | 20 ++++ .../kotlin/com/carriez/flutter_hbb/common.kt | 9 +- flutter/lib/consts.dart | 6 +- flutter/lib/mobile/pages/settings_page.dart | 101 ++++++++++++++---- 5 files changed, 116 insertions(+), 21 deletions(-) diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 6f646b913..b3c655917 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) + } + SET_START_ON_BOOT_OPT -> { + try { + if (call.arguments is Boolean) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } finally { + result.success(false) + } + } else -> { result.error("-1", "No such method", null) } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index 0bc6c1c28..2d53ea010 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -37,9 +37,14 @@ const val REQ_REQUEST_MEDIA_PROJECTION = 201 const val RES_FAILED = -100 // Flutter channel -const val START_ACTION = "start_action"; -const val IGNORE_BATTERY_OPTIMIZATIONS = "ignore_battery_optimizations"; +const val START_ACTION = "start_action" +const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt" +const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt" +const val IGNORE_BATTERY_OPTIMIZATIONS = "ignore_battery_optimizations" + +const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES" +const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT" @SuppressLint("ConstantLocale") val LOCAL_NAME = Locale.getDefault().toString() diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index a0766a874..1dc1c0b5a 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -150,7 +150,11 @@ const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW"; const kIgnoreBatteryOptimizations = "ignore_battery_optimizations"; /// Android channel invoke type key -const kStartAction = "start_action"; +class AndroidChannel { + static final kStartAction = "start_action"; + static final kGetStartOnBootOpt = "get_start_on_boot_opt"; + static final kSetStartOnBootOpt = "set_start_on_boot_opt"; +} /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 6bdb7e813..0e160396b 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -32,18 +32,21 @@ class SettingsPage extends StatefulWidget implements PageShape { } const url = 'https://rustdesk.com/'; -final _hasIgnoreBattery = androidVersion >= 26; -var _ignoreBatteryOpt = false; -var _enableAbr = false; -var _denyLANDiscovery = false; -var _onlyWhiteList = false; -var _enableDirectIPAccess = false; -var _enableRecordSession = false; -var _autoRecordIncomingSession = false; -var _localIP = ""; -var _directAccessPort = ""; class _SettingsState extends State with WidgetsBindingObserver { + final _hasIgnoreBattery = androidVersion >= 26; + var _ignoreBatteryOpt = false; + var _systemAlertWindow = false; + var _enableStartOnBoot = false; + var _enableAbr = false; + var _denyLANDiscovery = false; + var _onlyWhiteList = false; + var _enableDirectIPAccess = false; + var _enableRecordSession = false; + var _autoRecordIncomingSession = false; + var _localIP = ""; + var _directAccessPort = ""; + @override void initState() { super.initState(); @@ -51,11 +54,35 @@ class _SettingsState extends State with WidgetsBindingObserver { () async { var update = false; + if (_hasIgnoreBattery) { - update = await updateIgnoreBatteryStatus(); + if (await checkAndUpdateIgnoreBatteryStatus()) { + update = true; + } } - final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N"; + if (await checkAndUpdateSystemAlertWindow()) { + update = true; + } + + // TODO need input + // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW + var enableStartOnBoot = + await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt); + if (enableStartOnBoot) { + if (!canStartOnBoot()) { + enableStartOnBoot = false; + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); + } + } + + if (enableStartOnBoot != _enableStartOnBoot) { + update = true; + _enableStartOnBoot = enableStartOnBoot; + } + + final enableAbrRes = option2bool( + "enable-abr", await bind.mainGetOption(key: "enable-abr")); if (enableAbrRes != _enableAbr) { update = true; _enableAbr = enableAbrRes; @@ -126,14 +153,15 @@ class _SettingsState extends State with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { () async { - if (await updateIgnoreBatteryStatus()) { + if (await checkAndUpdateIgnoreBatteryStatus() || + await checkAndUpdateSystemAlertWindow()) { setState(() {}); } }(); } } - Future updateIgnoreBatteryStatus() async { + Future checkAndUpdateIgnoreBatteryStatus() async { final res = await AndroidPermissionManager.check(kIgnoreBatteryOptimizations); if (_ignoreBatteryOpt != res) { @@ -144,6 +172,16 @@ class _SettingsState extends State with WidgetsBindingObserver { } } + Future checkAndUpdateSystemAlertWindow() async { + final res = await AndroidPermissionManager.check(kSystemAlertWindow); + if (_systemAlertWindow != res) { + _systemAlertWindow = res; + return true; + } else { + return false; + } + } + @override Widget build(BuildContext context) { Provider.of(context); @@ -267,8 +305,8 @@ class _SettingsState extends State with WidgetsBindingObserver { ]), onToggle: (v) async { if (v) { - gFFI.invokeMethod( - kStartAction, kActionRequestIgnoreBatteryOptimizations); + gFFI.invokeMethod(AndroidChannel.kStartAction, + kActionRequestIgnoreBatteryOptimizations); } else { final res = await gFFI.dialogManager .show((setState, close) => CustomAlertDialog( @@ -285,12 +323,27 @@ class _SettingsState extends State with WidgetsBindingObserver { ], )); if (res == true) { - gFFI.invokeMethod( - kStartAction, kActionApplicationDetailsSettings); + gFFI.invokeMethod(AndroidChannel.kStartAction, + kActionApplicationDetailsSettings); } } })); } + enhancementsTiles.add(SettingsTile.switchTile( + initialValue: _enableStartOnBoot, + title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text("$translate('Start on Boot') (beta)"), + Text( + '* ${translate('Start the screen recording service on boot, which requires special permissions')}', + style: Theme.of(context).textTheme.bodySmall), + ]), + onToggle: (v) async { + if (v) { + // TODO + } else { + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); + } + })); return SettingsList( sections: [ @@ -391,6 +444,18 @@ class _SettingsState extends State with WidgetsBindingObserver { ], ); } + + bool canStartOnBoot() { + // TODO need input + // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW + if (_hasIgnoreBattery && !_ignoreBatteryOpt) { + return false; + } + if (!_systemAlertWindow) { + return false; + } + return true; + } } void showServerSettings(OverlayDialogManager dialogManager) async { From eebe3448786b1704d9be4c2b08e9d7de619511da Mon Sep 17 00:00:00 2001 From: Andi Ariffin Date: Tue, 28 Feb 2023 11:57:39 +0700 Subject: [PATCH 687/734] Fix typos causing gen_js_from_hbb.py to fail --- src/lang/en.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/en.rs b/src/lang/en.rs index 3e87cd661..250530013 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -39,8 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."), ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), - ("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."), + ("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."), ("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."), From 6bd26ef3987db43cadfecb19267262c8d5cf50f1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 28 Feb 2023 13:23:27 +0800 Subject: [PATCH 688/734] fix: linux canvas offset --- flutter/lib/models/model.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index fd97b1e5c..cea6785fc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -731,8 +731,15 @@ class CanvasModel with ChangeNotifier { Size getSize() { final size = MediaQueryData.fromWindow(ui.window).size; // If minimized, w or h may be negative here. - double w = size.width - windowBorderWidth * 2; - double h = size.height - tabBarHeight - windowBorderWidth * 2; + double w = size.width - + windowBorderWidth * 2 - + kDragToResizeAreaPadding.left - + kDragToResizeAreaPadding.right; + double h = size.height - + tabBarHeight - + windowBorderWidth * 2 - + kDragToResizeAreaPadding.top - + kDragToResizeAreaPadding.bottom; return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); } From e26707e55277cfb82644e8b471466ab0fde65994 Mon Sep 17 00:00:00 2001 From: sjpark Date: Tue, 28 Feb 2023 15:47:44 +0900 Subject: [PATCH 689/734] delete unused patch --- Cargo.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d768005fe..f93f776a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,8 +166,3 @@ panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip rpath = true - -[patch."https://github.com/fufesou/rdev"] -#rdev = { path = "../rdev" } -rdev = { git = "https://github.com/sj6219/rdev", branch = "sigma" } - From 8703d23277e66b970d52bb43d7e7d360051e47e4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 28 Feb 2023 14:50:51 +0800 Subject: [PATCH 690/734] refact canvas position and size Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 15 ++++++------ flutter/lib/models/input_model.dart | 4 ++-- flutter/lib/models/model.dart | 24 ++++++++++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c27546d9f..4b0215703 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -953,12 +953,13 @@ class _DisplayMenuState extends State<_DisplayMenu> { final canvasModel = widget.ffi.canvasModel; final width = (canvasModel.getDisplayWidth() * canvasModel.scale + - canvasModel.windowBorderWidth * 2) * + CanvasModel.leftToEdge + + CanvasModel.rightToEdge) * scale + magicWidth; final height = (canvasModel.getDisplayHeight() * canvasModel.scale + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * + CanvasModel.topToEdge + + CanvasModel.bottomToEdge) * scale + magicHeight; double left = wndRect.left + (wndRect.width - width) / 2; @@ -1027,10 +1028,10 @@ class _DisplayMenuState extends State<_DisplayMenu> { final canvasModel = widget.ffi.canvasModel; final displayWidth = canvasModel.getDisplayWidth(); final displayHeight = canvasModel.getDisplayHeight(); - final requiredWidth = displayWidth + - (canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2); - final requiredHeight = displayHeight + - (canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2); + final requiredWidth = + CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge; + final requiredHeight = + CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge; return selfWidth > (requiredWidth * scale) && selfHeight > (requiredHeight * scale); } diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index b91453138..8c6a89377 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -459,8 +459,8 @@ class InputModel { } evt['type'] = type; if (isDesktop) { - y = y - stateGlobal.tabBarHeight - stateGlobal.windowBorderWidth.value; - x -= stateGlobal.windowBorderWidth.value; + y -= CanvasModel.topToEdge; + x -= CanvasModel.leftToEdge; } final canvasModel = parent.target!.canvasModel; final nearThr = 3; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index cea6785fc..6def57462 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -727,19 +727,21 @@ class CanvasModel with ChangeNotifier { double get scrollX => _scrollX; double get scrollY => _scrollY; + static double get leftToEdge => + windowBorderWidth + kDragToResizeAreaPadding.left; + static double get rightToEdge => + windowBorderWidth + kDragToResizeAreaPadding.right; + static double get topToEdge => + tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top; + static double get bottomToEdge => + windowBorderWidth + kDragToResizeAreaPadding.bottom; + updateViewStyle() async { Size getSize() { final size = MediaQueryData.fromWindow(ui.window).size; // If minimized, w or h may be negative here. - double w = size.width - - windowBorderWidth * 2 - - kDragToResizeAreaPadding.left - - kDragToResizeAreaPadding.right; - double h = size.height - - tabBarHeight - - windowBorderWidth * 2 - - kDragToResizeAreaPadding.top - - kDragToResizeAreaPadding.bottom; + double w = size.width - leftToEdge - rightToEdge; + double h = size.height - topToEdge - bottomToEdge; return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); } @@ -813,8 +815,8 @@ class CanvasModel with ChangeNotifier { return parent.target?.ffiModel.display.height ?? defaultHeight; } - double get windowBorderWidth => stateGlobal.windowBorderWidth.value; - double get tabBarHeight => stateGlobal.tabBarHeight; + static double get windowBorderWidth => stateGlobal.windowBorderWidth.value; + static double get tabBarHeight => stateGlobal.tabBarHeight; moveDesktopMouse(double x, double y) { if (size.width == 0 || size.height == 0) { From 409da145c3138ff30c8372d9a88ac2842f467a4b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 28 Feb 2023 15:23:25 +0800 Subject: [PATCH 691/734] add settings page left side to 200 to conform to main page --- 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 e041b591d..52f64c0e3 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -19,7 +19,7 @@ import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; import '../../common/widgets/login.dart'; -const double _kTabWidth = 235; +const double _kTabWidth = 200; const double _kTabHeight = 42; const double _kCardFixedWidth = 540; const double _kCardLeftMargin = 15; From 45de6e3f66a7930097f1914aa818eff4091f0db9 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 28 Feb 2023 15:28:11 +0800 Subject: [PATCH 692/734] update Cargo.lock for new merge --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index a2cdf91a4..8f8895bd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4656,7 +4656,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc" +source = "git+https://github.com/fufesou/rdev#25a99ce71ab42843ad253dd51e6a35e83e87a8a4" dependencies = [ "cocoa", "core-foundation 0.9.3", From f65a40f914225052184d1b1b039a7e1b80aff947 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 28 Feb 2023 16:21:14 +0800 Subject: [PATCH 693/734] chore: try to enable keyboard for scroll, but behavior is strange on mac, #3428 issue --- flutter/lib/desktop/widgets/scroll_wrapper.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart index 32ed149e5..c5bc3394b 100644 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ b/flutter/lib/desktop/widgets/scroll_wrapper.dart @@ -14,6 +14,7 @@ class DesktopScrollWrapper extends StatelessWidget { return ImprovedScrolling( scrollController: scrollController, enableCustomMouseWheelScrolling: true, + // enableKeyboardScrolling: true, // strange behavior on mac customMouseWheelScrollConfig: CustomMouseWheelScrollConfig( scrollDuration: kDefaultScrollDuration, scrollCurve: Curves.linearToEaseOut, From 1b396d22879f1143f55080e5f19e5888fd355819 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 28 Feb 2023 17:25:59 +0800 Subject: [PATCH 694/734] opt: scrollbar in night mode --- flutter/lib/common.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 53a401230..023fe7511 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -217,6 +217,9 @@ class MyTheme { tabBarTheme: const TabBarTheme( labelColor: Colors.white70, ), + scrollbarTheme: ScrollbarThemeData( + thumbColor: MaterialStateProperty.all(Colors.grey[500]) + ), splashColor: Colors.transparent, highlightColor: Colors.transparent, splashFactory: isDesktop ? NoSplash.splashFactory : null, From a974fef1e50e456a592a7ea37a6271829d03567e Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Tue, 28 Feb 2023 13:19:02 +0330 Subject: [PATCH 695/734] 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 0c31e1531..f76567ee5 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -454,8 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), ("Reconnect", "اتصال مجدد"), - ("No transfers in progress", ""), - ("Codec", ""), - ("Resolution", ""), + ("No transfers in progress", "هیچ انتقالی در حال انجام نیست"), + ("Codec", "کدک"), + ("Resolution", "وضوح"), ].iter().cloned().collect(); } From 73bc963311475f8433dfede2f292cd6d05d9e9fc Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 28 Feb 2023 19:46:41 +0900 Subject: [PATCH 696/734] add kActionAccessibilitySettings to manage Input Permission --- flutter/lib/common.dart | 5 +++++ flutter/lib/consts.dart | 2 ++ flutter/lib/mobile/pages/settings_page.dart | 4 ++-- flutter/lib/models/server_model.dart | 10 +++------- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 41ac595f2..89b2c1b28 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -926,6 +926,11 @@ class AndroidPermissionManager { return gFFI.invokeMethod("check_permission", type); } + // startActivity goto Android Setting's page to request permission manually by user + static void startAction(String action) { + gFFI.invokeMethod(AndroidChannel.kStartAction, action); + } + static Future request(String type) { if (isDesktop) { return Future.value(true); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 1dc1c0b5a..3edafbf62 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -142,6 +142,8 @@ const kActionApplicationDetailsSettings = "android.settings.APPLICATION_DETAILS_SETTINGS"; const kActionRequestIgnoreBatteryOptimizations = "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; +const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS"; + const kRecordAudio = "android.permission.RECORD_AUDIO"; const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE"; const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW"; diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 0e160396b..397c117f7 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -305,7 +305,7 @@ class _SettingsState extends State with WidgetsBindingObserver { ]), onToggle: (v) async { if (v) { - gFFI.invokeMethod(AndroidChannel.kStartAction, + AndroidPermissionManager.startAction( kActionRequestIgnoreBatteryOptimizations); } else { final res = await gFFI.dialogManager @@ -323,7 +323,7 @@ class _SettingsState extends State with WidgetsBindingObserver { ], )); if (res == true) { - gFFI.invokeMethod(AndroidChannel.kStartAction, + AndroidPermissionManager.startAction( kActionApplicationDetailsSettings); } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 433990a2a..7ee23ec40 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -234,7 +234,7 @@ class ServerModel with ChangeNotifier { if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) { final res = await AndroidPermissionManager.request(kRecordAudio); if (!res) { - // TODO handle fail + showToast(translate('Failed')); return; } } @@ -250,7 +250,7 @@ class ServerModel with ChangeNotifier { final res = await AndroidPermissionManager.request(kManageExternalStorage); if (!res) { - // TODO handle fail + showToast(translate('Failed')); return; } } @@ -348,10 +348,6 @@ class ServerModel with ChangeNotifier { } } - Future initInput() async { - await parent.target?.invokeMethod("init_input"); - } - Future setPermanentPassword(String newPW) async { await bind.mainSetPermanentPassword(password: newPW); await Future.delayed(Duration(milliseconds: 500)); @@ -689,7 +685,7 @@ String getLoginDialogTag(int id) { showInputWarnAlert(FFI ffi) { ffi.dialogManager.show((setState, close) { submit() { - ffi.serverModel.initInput(); + AndroidPermissionManager.startAction(kActionAccessibilitySettings); close(); } From a071700cc7c77e9fa50bf44341a4e8eb3796d6a9 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 28 Feb 2023 19:25:14 +0800 Subject: [PATCH 697/734] fix build.py for mac --- build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index b30f76f95..8d64553c2 100755 --- a/build.py +++ b/build.py @@ -317,11 +317,11 @@ def build_flutter_dmg(version, features): "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") # ffi_bindgen_function_refactor() # limitations from flutter rust bridge - system2('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') + system2('sed -i "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') os.chdir('flutter') system2('flutter build macos --release') system2( - "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") + "create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.chdir("..") From 60ab29ad6e9ec80b9ce50abb073ac6bc384f8230 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 28 Feb 2023 21:02:42 +0900 Subject: [PATCH 698/734] 1. use XXPermissions to manage REQUEST_IGNORE_BATTERY_OPTIMIZATIONS. 2. pre-request permission on Start on Boot enabled. --- .../com/carriez/flutter_hbb/MainActivity.kt | 14 +------ .../kotlin/com/carriez/flutter_hbb/common.kt | 17 +++----- flutter/lib/common.dart | 13 ++++--- flutter/lib/consts.dart | 7 +--- flutter/lib/mobile/pages/settings_page.dart | 39 +++++++++++++------ 5 files changed, 43 insertions(+), 47 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 126e169da..e4b42cf08 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -18,6 +18,7 @@ import android.provider.Settings import android.util.Log import android.view.WindowManager import androidx.annotation.RequiresApi +import com.hjq.permissions.XXPermissions import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel @@ -76,7 +77,7 @@ class MainActivity : FlutterActivity() { } "check_permission" -> { if (call.arguments is String) { - result.success(checkPermission(context, call.arguments as String)) + result.success(XXPermissions.isGranted(context, call.arguments as String)) } else { result.success(false) } @@ -115,10 +116,6 @@ class MainActivity : FlutterActivity() { ) result.success(true) } - "init_input" -> { - initInput() - result.success(true) - } "stop_input" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { InputService.ctx?.disableSelf() @@ -177,13 +174,6 @@ class MainActivity : FlutterActivity() { } } - private fun initInput() { - val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent) - } - } - override fun onResume() { super.onResume() val inputPer = InputService.isOpen diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index 2d53ea010..0c34c2223 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -13,6 +13,7 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.os.PowerManager +import android.provider.Settings import android.provider.Settings.* import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat.getSystemService @@ -41,8 +42,6 @@ const val START_ACTION = "start_action" const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt" const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt" -const val IGNORE_BATTERY_OPTIMIZATIONS = "ignore_battery_optimizations" - const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES" const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT" @@ -70,21 +69,15 @@ fun requestPermission(context: Context, type: String) { } } -@RequiresApi(Build.VERSION_CODES.M) -fun checkPermission(context: Context, type: String): Boolean { - if (IGNORE_BATTERY_OPTIMIZATIONS == type) { - val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager - return pw.isIgnoringBatteryOptimizations(context.packageName) - } - return XXPermissions.isGranted(context, type) -} - @RequiresApi(Build.VERSION_CODES.M) fun startAction(context: Context, action: String) { try { context.startActivity(Intent(action).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - data = Uri.parse("package:" + context.packageName) + // don't pass package name when launch ACTION_ACCESSIBILITY_SETTINGS + if (ACTION_ACCESSIBILITY_SETTINGS != action) { + data = Uri.parse("package:" + context.packageName) + } }) } catch (e: Exception) { e.printStackTrace() diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 89b2c1b28..155fcc743 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -931,6 +931,8 @@ class AndroidPermissionManager { gFFI.invokeMethod(AndroidChannel.kStartAction, action); } + /// We use XXPermissions to request permissions, + /// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java static Future request(String type) { if (isDesktop) { return Future.value(true); @@ -938,17 +940,16 @@ class AndroidPermissionManager { gFFI.invokeMethod("request_permission", type); - // kIgnoreBatteryOptimizations permission doesn't depend on callback result, the result will be checked and updated on page resume - if (type == kIgnoreBatteryOptimizations) { - return Future.value(false); + // clear last task + if (_completer?.isCompleted == false) { + _completer?.complete(false); } + _timer?.cancel(); _current = type; _completer = Completer(); - // timeout - _timer?.cancel(); - _timer = Timer(Duration(seconds: 60), () { + _timer = Timer(Duration(seconds: 120), () { if (_completer == null) return; if (!_completer!.isCompleted) { _completer!.complete(false); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 3edafbf62..b075ee76f 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -140,17 +140,14 @@ const kIgnoreDpi = true; /// Android constants const kActionApplicationDetailsSettings = "android.settings.APPLICATION_DETAILS_SETTINGS"; -const kActionRequestIgnoreBatteryOptimizations = - "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS"; const kRecordAudio = "android.permission.RECORD_AUDIO"; const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE"; +const kRequestIgnoreBatteryOptimizations = + "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW"; -/// [kIgnoreBatteryOptimizations] not a Android Permission, it is a custom key, used in `ignore battery optimizations` check -const kIgnoreBatteryOptimizations = "ignore_battery_optimizations"; - /// Android channel invoke type key class AndroidChannel { static final kStartAction = "start_action"; diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 397c117f7..d4887bb58 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -65,7 +65,6 @@ class _SettingsState extends State with WidgetsBindingObserver { update = true; } - // TODO need input // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW var enableStartOnBoot = await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt); @@ -162,8 +161,8 @@ class _SettingsState extends State with WidgetsBindingObserver { } Future checkAndUpdateIgnoreBatteryStatus() async { - final res = - await AndroidPermissionManager.check(kIgnoreBatteryOptimizations); + final res = await AndroidPermissionManager.check( + kRequestIgnoreBatteryOptimizations); if (_ignoreBatteryOpt != res) { _ignoreBatteryOpt = res; return true; @@ -305,8 +304,8 @@ class _SettingsState extends State with WidgetsBindingObserver { ]), onToggle: (v) async { if (v) { - AndroidPermissionManager.startAction( - kActionRequestIgnoreBatteryOptimizations); + await AndroidPermissionManager.request( + kRequestIgnoreBatteryOptimizations); } else { final res = await gFFI.dialogManager .show((setState, close) => CustomAlertDialog( @@ -332,17 +331,34 @@ class _SettingsState extends State with WidgetsBindingObserver { enhancementsTiles.add(SettingsTile.switchTile( initialValue: _enableStartOnBoot, title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("$translate('Start on Boot') (beta)"), + Text("${translate('Start on Boot')} (beta)"), Text( '* ${translate('Start the screen recording service on boot, which requires special permissions')}', style: Theme.of(context).textTheme.bodySmall), ]), - onToggle: (v) async { - if (v) { - // TODO - } else { - gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); + onToggle: (toValue) async { + if (toValue) { + // 1. request kIgnoreBatteryOptimizations + if (!await AndroidPermissionManager.check( + kRequestIgnoreBatteryOptimizations)) { + if (!await AndroidPermissionManager.request( + kRequestIgnoreBatteryOptimizations)) { + return; + } + } + + // 2. request kSystemAlertWindow + if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { + if (!await AndroidPermissionManager.request(kSystemAlertWindow)) { + return; + } + } + + // (Optional) 3. request input permission } + setState(() => _enableStartOnBoot = toValue); + + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue); })); return SettingsList( @@ -446,7 +462,6 @@ class _SettingsState extends State with WidgetsBindingObserver { } bool canStartOnBoot() { - // TODO need input // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW if (_hasIgnoreBattery && !_ignoreBatteryOpt) { return false; From fe262abc5d75f1bc92829680314190ca13a7052e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 28 Feb 2023 20:15:14 +0800 Subject: [PATCH 699/734] remove useless code --- build.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/build.py b/build.py index 8d64553c2..45fe1b132 100755 --- a/build.py +++ b/build.py @@ -315,9 +315,6 @@ def build_flutter_dmg(version, features): # copy dylib system2( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") - # ffi_bindgen_function_refactor() - # limitations from flutter rust bridge - system2('sed -i "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') os.chdir('flutter') system2('flutter build macos --release') system2( From 836249d34cd01277b3d851a62f104cca9c9a600b Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 28 Feb 2023 21:48:40 +0900 Subject: [PATCH 700/734] refactor initFlutterChannel --- .../com/carriez/flutter_hbb/MainActivity.kt | 255 +++++++++--------- .../kotlin/com/carriez/flutter_hbb/common.kt | 2 - 2 files changed, 125 insertions(+), 132 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index e4b42cf08..be8c857ce 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -13,8 +13,6 @@ import android.content.Intent import android.content.ServiceConnection import android.os.Build import android.os.IBinder -import android.preference.PreferenceManager -import android.provider.Settings import android.util.Log import android.view.WindowManager import androidx.annotation.RequiresApi @@ -44,134 +42,8 @@ class MainActivity : FlutterActivity() { flutterMethodChannel = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, channelTag - ).apply { - // make sure result is set, otherwise flutter will await forever - setMethodCallHandler { call, result -> - when (call.method) { - "init_service" -> { - Intent(activity, MainService::class.java).also { - bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) - } - if (MainService.isReady) { - result.success(false) - return@setMethodCallHandler - } - requestMediaProjection() - result.success(true) - } - "start_capture" -> { - mainService?.let { - result.success(it.startCapture()) - } ?: let { - result.success(false) - } - } - "stop_service" -> { - Log.d(logTag, "Stop service") - mainService?.let { - it.destroy() - result.success(true) - } ?: let { - result.success(false) - } - } - "check_permission" -> { - if (call.arguments is String) { - result.success(XXPermissions.isGranted(context, call.arguments as String)) - } else { - result.success(false) - } - } - "request_permission" -> { - if (call.arguments is String) { - requestPermission(context, call.arguments as String) - result.success(true) - } else { - result.success(false) - } - } - START_ACTION -> { - if (call.arguments is String) { - startAction(context, call.arguments as String) - result.success(true) - } else { - result.success(false) - } - } - "check_video_permission" -> { - mainService?.let { - result.success(it.checkMediaPermission()) - } ?: let { - result.success(false) - } - } - "check_service" -> { - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "input", "value" to InputService.isOpen.toString()) - ) - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "media", "value" to MainService.isReady.toString()) - ) - result.success(true) - } - "stop_input" -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - InputService.ctx?.disableSelf() - } - InputService.ctx = null - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "input", "value" to InputService.isOpen.toString()) - ) - result.success(true) - } - "cancel_notification" -> { - try { - val id = call.arguments as Int - mainService?.cancelNotification(id) - } finally { - result.success(true) - } - } - "enable_soft_keyboard" -> { - // https://blog.csdn.net/hanye2020/article/details/105553780 - try { - if (call.arguments as Boolean) { - window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - } else { - window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - } - } finally { - result.success(true) - } - } - GET_START_ON_BOOT_OPT -> { - val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) - result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) - } - SET_START_ON_BOOT_OPT -> { - try { - if (call.arguments is Boolean) { - val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) - val edit = prefs.edit() - edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean) - edit.apply() - result.success(true) - } else { - result.success(false) - } - } finally { - result.success(false) - } - } - else -> { - result.error("-1", "No such method", null) - } - } - } - } + ) + initFlutterChannel(flutterMethodChannel) } override fun onResume() { @@ -219,4 +91,127 @@ class MainActivity : FlutterActivity() { mainService = null } } + + private fun initFlutterChannel(flutterMethodChannel: MethodChannel) { + flutterMethodChannel.setMethodCallHandler { call, result -> + // make sure result will be invoked, otherwise flutter will await forever + when (call.method) { + "init_service" -> { + Intent(activity, MainService::class.java).also { + bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) + } + if (MainService.isReady) { + result.success(false) + return@setMethodCallHandler + } + requestMediaProjection() + result.success(true) + } + "start_capture" -> { + mainService?.let { + result.success(it.startCapture()) + } ?: let { + result.success(false) + } + } + "stop_service" -> { + Log.d(logTag, "Stop service") + mainService?.let { + it.destroy() + result.success(true) + } ?: let { + result.success(false) + } + } + "check_permission" -> { + if (call.arguments is String) { + result.success(XXPermissions.isGranted(context, call.arguments as String)) + } else { + result.success(false) + } + } + "request_permission" -> { + if (call.arguments is String) { + requestPermission(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } + START_ACTION -> { + if (call.arguments is String) { + startAction(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } + "check_video_permission" -> { + mainService?.let { + result.success(it.checkMediaPermission()) + } ?: let { + result.success(false) + } + } + "check_service" -> { + Companion.flutterMethodChannel.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + Companion.flutterMethodChannel.invokeMethod( + "on_state_changed", + mapOf("name" to "media", "value" to MainService.isReady.toString()) + ) + result.success(true) + } + "stop_input" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + InputService.ctx?.disableSelf() + } + InputService.ctx = null + Companion.flutterMethodChannel.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + result.success(true) + } + "cancel_notification" -> { + if (call.arguments is Int) { + val id = call.arguments as Int + mainService?.cancelNotification(id) + } else { + result.success(true) + } + } + "enable_soft_keyboard" -> { + // https://blog.csdn.net/hanye2020/article/details/105553780 + if (call.arguments as Boolean) { + window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + } + result.success(true) + + } + GET_START_ON_BOOT_OPT -> { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) + } + SET_START_ON_BOOT_OPT -> { + if (call.arguments is Boolean) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } + else -> { + result.error("-1", "No such method", null) + } + } + } + } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index 0c34c2223..6970fd137 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -53,7 +53,6 @@ data class Info( var width: Int, var height: Int, var scale: Int, var dpi: Int ) -@RequiresApi(Build.VERSION_CODES.M) fun requestPermission(context: Context, type: String) { XXPermissions.with(context) .permission(type) @@ -69,7 +68,6 @@ fun requestPermission(context: Context, type: String) { } } -@RequiresApi(Build.VERSION_CODES.M) fun startAction(context: Context, action: String) { try { context.startActivity(Intent(action).apply { From bf0e0d20c308246f266590a04dee14b8ffc45a9f Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 28 Feb 2023 14:08:55 +0100 Subject: [PATCH 701/734] improved readability --- flutter/lib/common.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 3680b0a11..29d4a195d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -170,8 +170,13 @@ class MyTheme { scaffoldBackgroundColor: Color(0xFFFFFFFF), dialogBackgroundColor: Color(0xFFFFFFFF), dialogTheme: DialogTheme( + elevation: 15, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(18.0), + side: BorderSide( + width: 1, + color: Color(0xFFEEEEEE), + ), ), ), inputDecorationTheme: InputDecorationTheme( @@ -257,8 +262,13 @@ class MyTheme { scaffoldBackgroundColor: Color(0xFF18191E), dialogBackgroundColor: Color(0xFF18191E), dialogTheme: DialogTheme( + elevation: 15, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(18.0), + side: BorderSide( + width: 1, + color: Color(0xFF24252B), + ), ), ), inputDecorationTheme: InputDecorationTheme( @@ -559,7 +569,7 @@ class OverlayDialogManager { BackButtonInterceptor.removeByName(dialogTag); } - dialog.entry = OverlayEntry(builder: (_) { + dialog.entry = OverlayEntry(builder: (context) { bool innerClicked = false; return Listener( onPointerUp: (_) { @@ -569,7 +579,9 @@ class OverlayDialogManager { innerClicked = false; }, child: Container( - color: Colors.black12, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black12 + : Colors.black45, child: StatefulBuilder(builder: (context, setState) { return Listener( onPointerUp: (_) => innerClicked = true, From 561d2bfb1f870ee34c645148ff9f8a1dd8ead900 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 28 Feb 2023 14:10:36 +0100 Subject: [PATCH 702/734] removed useless buttonShape --- flutter/lib/desktop/pages/file_manager_page.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index f276961b0..9210a30c1 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -49,11 +49,6 @@ enum MouseFocusScope { none } -final buttonShape = - MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), -)); - class FileManagerPage extends StatefulWidget { const FileManagerPage({Key? key, required this.id, this.forceRelay}) : super(key: key); @@ -1066,7 +1061,6 @@ class _FileManagerPageState extends State ? MyTheme.accent80 : MyTheme.accent, ), - shape: buttonShape, ), onPressed: validItems(selectedItems) ? () { From baea9e529d5b32740014a55c6b4fb769f8614cf4 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 28 Feb 2023 14:21:27 +0100 Subject: [PATCH 703/734] modern rename peer dialog --- flutter/lib/common/widgets/peer_card.dart | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index a69fc3bbe..1d3a18c7c 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -682,21 +682,30 @@ abstract class BasePeerCard extends StatelessWidget { child: TextFormField( controller: controller, autofocus: true, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: translate('Name')), + decoration: InputDecoration(labelText: translate('Name')), ), ), ), Obx(() => Offstage( offstage: isInProgress.isFalse, child: const LinearProgressIndicator())), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + icon: Icon(Icons.close_rounded), + label: Text(translate("Cancel")), + onPressed: close, + ), + ElevatedButton.icon( + icon: Icon(Icons.done_rounded), + label: Text(translate("Ok")), + onPressed: submit, + ), + ], + ).paddingOnly(top: 20) ], ), - actions: [ - dialogButton("Cancel", onPressed: close, isOutline: true), - dialogButton("OK", onPressed: submit), - ], onSubmit: submit, onCancel: close, ); From 660d6ff2302e4ad3a3a4091e267a066eef620693 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 28 Feb 2023 22:26:47 +0900 Subject: [PATCH 704/734] 1. fix check boot on start opt. 2. fix late var flutterMethodChannel --- .../com/carriez/flutter_hbb/BootReceiver.kt | 16 +++++++++++++ .../com/carriez/flutter_hbb/MainActivity.kt | 14 +++++------ .../com/carriez/flutter_hbb/MainService.kt | 4 ++-- .../kotlin/com/carriez/flutter_hbb/common.kt | 2 +- flutter/lib/mobile/pages/settings_page.dart | 24 ++++++++++--------- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt index a49dcc326..8f6767e58 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt @@ -1,11 +1,15 @@ package com.carriez.flutter_hbb +import android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +import android.Manifest.permission.SYSTEM_ALERT_WINDOW import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import android.widget.Toast +import com.hjq.permissions.XXPermissions +import io.flutter.embedding.android.FlutterActivity const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED" @@ -16,6 +20,18 @@ class BootReceiver : BroadcastReceiver() { Log.d(logTag, "onReceive ${intent.action}") if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) { + // check SharedPreferences config + val prefs = context.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) + if (!prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) { + Log.d(logTag, "KEY_START_ON_BOOT_OPT is false") + return + } + // check pre-permission + if (!XXPermissions.isGranted(context, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SYSTEM_ALERT_WINDOW)){ + Log.d(logTag, "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS or SYSTEM_ALERT_WINDOW is not granted") + return + } + val it = Intent(context, MainService::class.java).apply { action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index be8c857ce..79fb60790 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -24,7 +24,7 @@ import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { companion object { - lateinit var flutterMethodChannel: MethodChannel + var flutterMethodChannel: MethodChannel? = null } private val channelTag = "mChannel" @@ -43,14 +43,14 @@ class MainActivity : FlutterActivity() { flutterEngine.dartExecutor.binaryMessenger, channelTag ) - initFlutterChannel(flutterMethodChannel) + initFlutterChannel(flutterMethodChannel!!) } override fun onResume() { super.onResume() val inputPer = InputService.isOpen activity.runOnUiThread { - flutterMethodChannel.invokeMethod( + flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "input", "value" to inputPer.toString()) ) @@ -67,7 +67,7 @@ class MainActivity : FlutterActivity() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) { - flutterMethodChannel.invokeMethod("on_media_projection_canceled", null) + flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null) } } @@ -154,11 +154,11 @@ class MainActivity : FlutterActivity() { } } "check_service" -> { - Companion.flutterMethodChannel.invokeMethod( + Companion.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "input", "value" to InputService.isOpen.toString()) ) - Companion.flutterMethodChannel.invokeMethod( + Companion.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "media", "value" to MainService.isReady.toString()) ) @@ -169,7 +169,7 @@ class MainActivity : FlutterActivity() { InputService.ctx?.disableSelf() } InputService.ctx = null - Companion.flutterMethodChannel.invokeMethod( + Companion.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "input", "value" to InputService.isOpen.toString()) ) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index e28311964..e323d2951 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -409,13 +409,13 @@ class MainService : Service() { fun checkMediaPermission(): Boolean { Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "media", "value" to isReady.toString()) ) } Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "input", "value" to InputService.isOpen.toString()) ) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index 6970fd137..bd91d582c 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -59,7 +59,7 @@ fun requestPermission(context: Context, type: String) { .request { _, all -> if (all) { Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_android_permission_result", mapOf("type" to type, "result" to all) ) diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index d4887bb58..e54f66ffb 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -36,7 +36,6 @@ const url = 'https://rustdesk.com/'; class _SettingsState extends State with WidgetsBindingObserver { final _hasIgnoreBattery = androidVersion >= 26; var _ignoreBatteryOpt = false; - var _systemAlertWindow = false; var _enableStartOnBoot = false; var _enableAbr = false; var _denyLANDiscovery = false; @@ -61,7 +60,7 @@ class _SettingsState extends State with WidgetsBindingObserver { } } - if (await checkAndUpdateSystemAlertWindow()) { + if (await checkAndUpdateStartOnBoot()) { update = true; } @@ -69,7 +68,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var enableStartOnBoot = await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt); if (enableStartOnBoot) { - if (!canStartOnBoot()) { + if (!await canStartOnBoot()) { enableStartOnBoot = false; gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); } @@ -152,8 +151,9 @@ class _SettingsState extends State with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { () async { - if (await checkAndUpdateIgnoreBatteryStatus() || - await checkAndUpdateSystemAlertWindow()) { + final ibs = await checkAndUpdateIgnoreBatteryStatus(); + final sob = await checkAndUpdateStartOnBoot(); + if (ibs || sob) { setState(() {}); } }(); @@ -171,10 +171,12 @@ class _SettingsState extends State with WidgetsBindingObserver { } } - Future checkAndUpdateSystemAlertWindow() async { - final res = await AndroidPermissionManager.check(kSystemAlertWindow); - if (_systemAlertWindow != res) { - _systemAlertWindow = res; + Future checkAndUpdateStartOnBoot() async { + if (!await canStartOnBoot() && _enableStartOnBoot) { + _enableStartOnBoot = false; + debugPrint( + "checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false"); + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); return true; } else { return false; @@ -461,12 +463,12 @@ class _SettingsState extends State with WidgetsBindingObserver { ); } - bool canStartOnBoot() { + Future canStartOnBoot() async { // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW if (_hasIgnoreBattery && !_ignoreBatteryOpt) { return false; } - if (!_systemAlertWindow) { + if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { return false; } return true; From be2fa3e4443c3a12af5bc3541757878ce1389232 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 28 Feb 2023 22:32:51 +0900 Subject: [PATCH 705/734] fix restart service crash --- .../app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index e323d2951..6b6169c61 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -278,6 +278,7 @@ class MainService : Service() { Log.d("whichService", "this service: ${Thread.currentThread()}") super.onStartCommand(intent, flags, startId) if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) { + createForegroundNotification() Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager From 20c7075ddb06c102b70e780017e144c24bf8779e Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 28 Feb 2023 15:30:46 +0800 Subject: [PATCH 706/734] mobile, canvas size Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 6 ++---- flutter/lib/models/model.dart | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 8c6a89377..df9ad2585 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -458,10 +458,8 @@ class InputModel { return; } evt['type'] = type; - if (isDesktop) { - y -= CanvasModel.topToEdge; - x -= CanvasModel.leftToEdge; - } + y -= CanvasModel.topToEdge; + x -= CanvasModel.leftToEdge; final canvasModel = parent.target!.canvasModel; final nearThr = 3; var nearRight = (canvasModel.size.width - x) < nearThr; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6def57462..802a18a52 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -727,14 +727,18 @@ class CanvasModel with ChangeNotifier { double get scrollX => _scrollX; double get scrollY => _scrollY; - static double get leftToEdge => - windowBorderWidth + kDragToResizeAreaPadding.left; - static double get rightToEdge => - windowBorderWidth + kDragToResizeAreaPadding.right; - static double get topToEdge => - tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top; - static double get bottomToEdge => - windowBorderWidth + kDragToResizeAreaPadding.bottom; + static double get leftToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.left + : 0; + static double get rightToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.right + : 0; + static double get topToEdge => (isDesktop || isWebDesktop) + ? tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top + : 0; + static double get bottomToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.bottom + : 0; updateViewStyle() async { Size getSize() { From 75e3f1c3639f8067590d3ae0864138241930a546 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 28 Feb 2023 15:40:44 +0100 Subject: [PATCH 707/734] Added delete confermation dialog --- flutter/lib/common/widgets/peer_card.dart | 61 ++++++++++++++++++----- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 1d3a18c7c..13321db57 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -532,19 +532,7 @@ abstract class BasePeerCard extends StatelessWidget { ], ), proc: () { - () async { - if (isLan) { - bind.mainRemoveDiscovered(id: id); - } else { - final favs = (await bind.mainGetFav()).toList(); - if (favs.remove(id)) { - await bind.mainStoreFav(favs: favs); - } - await bind.mainRemovePeer(id: id); - } - removePreference(id); - await reloadFunc(); - }(); + _delete(id, isLan, reloadFunc); }, padding: menuPadding, dismissOnClicked: true, @@ -714,6 +702,53 @@ abstract class BasePeerCard extends StatelessWidget { @protected void _update(); + + void _delete(String id, bool isLan, Function reloadFunc) async { + gFFI.dialogManager.show( + (setState, close) { + submit() async { + if (isLan) { + bind.mainRemoveDiscovered(id: id); + } else { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + } + await bind.mainRemovePeer(id: id); + } + removePreference(id); + await reloadFunc(); + close(); + } + + return CustomAlertDialog( + title: Text(translate('Delete')), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + icon: Icon(Icons.close_rounded), + label: Text(translate("Cancel")), + onPressed: close, + ), + ElevatedButton.icon( + icon: Icon(Icons.done_rounded), + label: Text(translate("Ok")), + onPressed: submit, + ), + ], + ).paddingOnly(top: 20) + ], + ), + onSubmit: submit, + onCancel: close, + ); + }, + ); + } } class RecentPeerCard extends BasePeerCard { From 7bf728bdad47b10e43e9368c473c6a7effd909c3 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 28 Feb 2023 15:57:25 +0100 Subject: [PATCH 708/734] restart device dialog --- flutter/lib/common/widgets/peer_card.dart | 21 +++++++++++++-- flutter/lib/mobile/widgets/dialog.dart | 33 ++++++++++++++++------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 13321db57..cc5568bca 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -661,7 +661,13 @@ abstract class BasePeerCard extends StatelessWidget { } return CustomAlertDialog( - title: Text(translate('Rename')), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.edit_rounded, color: MyTheme.accent), + Text(translate('Rename')).paddingOnly(left: 10), + ], + ), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -722,7 +728,18 @@ abstract class BasePeerCard extends StatelessWidget { } return CustomAlertDialog( - title: Text(translate('Delete')), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.delete_rounded, + color: Colors.red, + ), + Text(translate('Delete')).paddingOnly( + left: 10, + ), + ], + ), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 931999382..fde9ac4ad 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -25,19 +25,32 @@ void showRestartRemoteDevice( final res = await dialogManager.show((setState, close) => CustomAlertDialog( title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Restart Remote Device")), + Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28), + Text(translate("Restart Remote Device")).paddingOnly(left: 10), ]), - content: Text( - "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + content: Column( + children: [ + Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + icon: Icon(Icons.close_rounded), + label: Text(translate("Cancel")), + onPressed: close, + ), + ElevatedButton.icon( + icon: Icon(Icons.done_rounded), + label: Text(translate("Ok")), + onPressed: () => close(true), + ), + ], + ).paddingOnly(top: 20) + ], + ), onCancel: close, onSubmit: () => close(true), - actions: [ - dialogButton("Cancel", onPressed: close, isOutline: true), - dialogButton("OK", onPressed: () => close(true)), - ], )); if (res == true) bind.sessionRestartRemoteDevice(id: id); } From 5375c98e25bcd1d4a7c51d6fcdcb70482312fcda Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 28 Feb 2023 23:04:15 +0800 Subject: [PATCH 709/734] to make macos debug can be run directly without flutter run to skip "mapped file has no Team ID and is not a platform binary (signed with custom identity or adhoc" issue --- flutter/macos/Runner.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 0019335ef..c73e666c7 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -487,7 +487,7 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; From f26088765e31e7da255633ba717913e86a699978 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 1 Mar 2023 00:05:06 +0900 Subject: [PATCH 710/734] 1. sync from flutter and pass app_dir from MainService.kt to backend server when app start on boot. 2. add start_service when start on boot. --- .../com/carriez/flutter_hbb/BootReceiver.kt | 1 + .../com/carriez/flutter_hbb/MainActivity.kt | 13 +++++++++++-- .../com/carriez/flutter_hbb/MainService.kt | 18 ++++++++++++++++-- .../kotlin/com/carriez/flutter_hbb/common.kt | 3 +++ flutter/lib/consts.dart | 1 + flutter/lib/main.dart | 1 + flutter/lib/models/native_model.dart | 10 +++++++--- src/flutter_ffi.rs | 18 ++++++++++++++++-- 8 files changed, 56 insertions(+), 9 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt index 8f6767e58..71bbba754 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt @@ -34,6 +34,7 @@ class BootReceiver : BroadcastReceiver() { val it = Intent(context, MainService::class.java).apply { action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE + putExtra(EXT_INIT_FROM_BOOT, true) } Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 79fb60790..52a5ff75e 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -15,7 +15,6 @@ import android.os.Build import android.os.IBinder import android.util.Log import android.view.WindowManager -import androidx.annotation.RequiresApi import com.hjq.permissions.XXPermissions import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine @@ -31,7 +30,6 @@ class MainActivity : FlutterActivity() { private val logTag = "mMainActivity" private var mainService: MainService? = null - @RequiresApi(Build.VERSION_CODES.M) override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) if (MainService.isReady) { @@ -208,6 +206,17 @@ class MainActivity : FlutterActivity() { result.success(false) } } + SYNC_APP_DIR_CONFIG_PATH -> { + if (call.arguments is String) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putString(KEY_APP_DIR_CONFIG_PATH, call.arguments as String) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } else -> { result.error("-1", "No such method", null) } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index 6b6169c61..fa7440c8d 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -35,6 +35,7 @@ import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import io.flutter.embedding.android.FlutterActivity import java.util.concurrent.Executors import kotlin.concurrent.thread import org.json.JSONException @@ -143,7 +144,11 @@ class MainService : Service() { // jvm call rust private external fun init(ctx: Context) - private external fun startServer() + + /// When app start on boot, app_dir will not be passed from flutter + /// so pass a app_dir here to rust server + private external fun startServer(app_dir: String) + private external fun startService() private external fun onVideoFrameUpdate(buf: ByteBuffer) private external fun onAudioFrameUpdate(buf: ByteBuffer) private external fun translateLocale(localeName: String, input: String): String @@ -199,7 +204,12 @@ class MainService : Service() { } updateScreenInfo(resources.configuration.orientation) initNotification() - startServer() + + // keep the config dir same with flutter + val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) + val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: "" + startServer(configPath) + createForegroundNotification() } @@ -279,6 +289,10 @@ class MainService : Service() { super.onStartCommand(intent, flags, startId) if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) { createForegroundNotification() + + if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) { + startService() + } Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index bd91d582c..f8ef07fd1 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -27,6 +27,7 @@ import java.util.* const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION" const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE" const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" +const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT" const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT" const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" @@ -41,9 +42,11 @@ const val RES_FAILED = -100 const val START_ACTION = "start_action" const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt" const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt" +const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir" const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES" const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT" +const val KEY_APP_DIR_CONFIG_PATH = "KEY_APP_DIR_CONFIG_PATH" @SuppressLint("ConstantLocale") val LOCAL_NAME = Locale.getDefault().toString() diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index b075ee76f..95e4d17e7 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -153,6 +153,7 @@ class AndroidChannel { static final kStartAction = "start_action"; static final kGetStartOnBootOpt = "get_start_on_boot_opt"; static final kSetStartOnBootOpt = "set_start_on_boot_opt"; + static final kSyncAppDirConfigPath = "sync_app_dir"; } /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index baf7193b3..6d3f863e7 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -153,6 +153,7 @@ void runMainApp(bool startService) async { void runMobileApp() async { await initEnv(kAppTypeMain); if (isAndroid) androidChannelInit(); + platformFFI.syncAndroidServiceAppDirConfigPath(); runApp(App()); } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 13f5b4587..28dc8085e 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,7 +30,7 @@ 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) +// pub fn session_register_texture(id: *const char, ptr: usize) typedef F6 = Void Function(Pointer, Uint64); typedef F6Dart = void Function(Pointer, int); @@ -56,7 +56,6 @@ class PlatformFFI { F4Dart? _session_get_rgba_size; F5Dart? _session_next_rgba; F6Dart? _session_register_texture; - static get localeName => Platform.localeName; @@ -162,7 +161,8 @@ class PlatformFFI { dylib.lookupFunction("session_get_rgba_size"); _session_next_rgba = dylib.lookupFunction("session_next_rgba"); - _session_register_texture = dylib.lookupFunction("session_register_texture"); + _session_register_texture = + dylib.lookupFunction("session_register_texture"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; @@ -301,4 +301,8 @@ class PlatformFFI { if (!isAndroid) return Future(() => false); return await _toAndroidChannel.invokeMethod(method, arguments); } + + void syncAndroidServiceAppDirConfigPath() { + invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir); + } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e49ba65f7..e5b24fa53 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1361,7 +1361,7 @@ pub fn send_url_scheme(_url: String) { #[cfg(target_os = "android")] pub mod server_side { - use hbb_common::log; + use hbb_common::{log, config}; use jni::{ objects::{JClass, JString}, sys::jstring, @@ -1374,11 +1374,25 @@ pub mod server_side { pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer( env: JNIEnv, _class: JClass, + app_dir: JString, ) { - log::debug!("startServer from java"); + log::debug!("startServer from jvm"); + if let Ok(app_dir) = env.get_string(app_dir) { + *config::APP_DIR.write().unwrap() = app_dir.into(); + } std::thread::spawn(move || start_server(true)); } + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startService( + env: JNIEnv, + _class: JClass, + ) { + log::debug!("startService from jvm"); + config::Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + #[no_mangle] pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_translateLocale( env: JNIEnv, From 2cb3ca4ed00a22d4f512e70f0202a6f05bc1be13 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 1 Mar 2023 00:14:37 +0900 Subject: [PATCH 711/734] update lang, start on boot --- flutter/lib/mobile/pages/settings_page.dart | 2 +- src/lang/ca.rs | 2 ++ src/lang/cn.rs | 2 ++ src/lang/cs.rs | 4 +++- src/lang/da.rs | 2 ++ src/lang/de.rs | 4 +++- src/lang/eo.rs | 2 ++ src/lang/es.rs | 2 ++ src/lang/fa.rs | 4 +++- src/lang/fr.rs | 2 ++ src/lang/gr.rs | 2 ++ src/lang/hu.rs | 2 ++ src/lang/id.rs | 2 ++ src/lang/it.rs | 4 +++- src/lang/ja.rs | 2 ++ src/lang/ko.rs | 2 ++ src/lang/kz.rs | 2 ++ src/lang/nl.rs | 2 ++ src/lang/pl.rs | 5 ++--- src/lang/pt_PT.rs | 4 +++- 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 ++ 34 files changed, 72 insertions(+), 9 deletions(-) diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index e54f66ffb..e07f8f59f 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -335,7 +335,7 @@ class _SettingsState extends State with WidgetsBindingObserver { title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("${translate('Start on Boot')} (beta)"), Text( - '* ${translate('Start the screen recording service on boot, which requires special permissions')}', + '* ${translate('Start the screen sharing service on boot, requires special permissions')}', style: Theme.of(context).textTheme.bodySmall), ]), onToggle: (toValue) async { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index aa33ae6e5..53ec69b5f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), ("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Connexió no disponible"), ("Legacy mode", "Mode heretat"), ("Map mode", "Mode mapa"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index f975e343f..4c037234b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "保持 RustDesk 后台服务"), ("Ignore Battery Optimizations", "忽略电池优化"), ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), + ("Start on Boot", "开机自启动"), + ("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), ("Map mode", "1:1 传输"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index cfe69924c..25a494eef 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), @@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), - ("No transfers in progress", ""), ("Codec", ""), ("Resolution", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 19310357b..8fd6f9be1 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), ("Ignore Battery Optimizations", "Ignorer betteri optimeringer"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Forbindelse ikke tilladt"), ("Legacy mode", "Bagudkompatibilitetstilstand"), ("Map mode", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 3d95832ec..754d7b9ef 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), ("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"), ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), ("Map mode", "Kartenmodus"), @@ -457,5 +459,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Codec", "Codec"), ("Resolution", "Auflösung"), ("No transfers in progress", "Keine Übertragungen im Gange"), - ].iter().cloned().collect(); + ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 9b7912cff..dfee4fb87 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index af0da0479..8477ba99b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 0c31e1531..2cefaa98f 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"), ("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"), ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "اتصال مجاز نیست"), ("Legacy mode", "legacy حالت"), ("Map mode", "map حالت"), @@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), ("Reconnect", "اتصال مجدد"), - ("No transfers in progress", ""), ("Codec", ""), ("Resolution", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 0e45827f7..28f1dd9d1 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Connexion non autorisée"), ("Legacy mode", "Mode hérité"), ("Map mode", ""), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index fca98f228..55a3c9bb7 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"), ("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Η σύνδεση απορρίφθηκε"), ("Legacy mode", "Λειτουργία συμβατότητας"), ("Map mode", "Map mode"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 437cf445a..f47d522db 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk futtatása a háttérben"), ("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívűl hagyása"), ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállítási oldalára, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "A csatlakozás nem engedélyezett"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 84892a7f8..7d02e154d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Pertahankan RustDesk berjalan pada background service"), ("Ignore Battery Optimizations", "Abaikan Pengoptimalan Baterai"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Koneksi tidak dijinkan"), ("Legacy mode", "Mode lama"), ("Map mode", "Mode peta"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 101685c4a..8aedc04f6 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"), ("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"), ("android_open_battery_optimizations_tip", "Se si desidera disabilitare questa funzione, andare nelle impostazioni dell'applicazione RustDesk, aprire la sezione [Batteria] e deselezionare [Senza restrizioni]."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Connessione non consentita"), ("Legacy mode", "Modalità legacy"), ("Map mode", "Modalità mappa"), @@ -454,8 +456,8 @@ 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"), - ("No transfers in progress", "Nessun trasferimento in corso"), ("Codec", "Codec"), ("Resolution", "Risoluzione"), + ("No transfers in progress", "Nessun trasferimento in corso"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c19b607ca..d097a8b61 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk バックグラウンドサービスを維持"), ("Ignore Battery Optimizations", "バッテリーの最適化を無効にする"), ("android_open_battery_optimizations_tip", "この機能を使わない場合は、次のRestDeskアプリ設定ページから「バッテリー」に進み、「制限なし」の選択を外してください"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "接続が許可されていません"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 97574e67d..8ca881f16 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk 백그라운드 서비스로 유지하기"), ("Ignore Battery Optimizations", "배터리 최적화 무시하기"), ("android_open_battery_optimizations_tip", "해당 기능을 비활성화하려면 RustDesk 응용 프로그램 설정 페이지로 이동하여 [배터리]에서 [제한 없음] 선택을 해제하십시오."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "연결이 허용되지 않음"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 54a51b439..a9acdce65 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Қосылу рұқсат етілмеген"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index f38c14791..cf3eb430c 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Verbinding niet toegestaan"), ("Legacy mode", "Verouderde modus"), ("Map mode", "Map mode"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 13027a682..a0808f5bf 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"), ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"), ("android_open_battery_optimizations_tip", "Jeśli chcesz wyłączyć tę funkcję, przejdź do następnej strony ustawień aplikacji RustDesk, znajdź i wprowadź [Bateria], odznacz [Bez ograniczeń]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Połączenie niedozwolone"), ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), ("Map mode", "Tryb mapowania"), @@ -456,9 +458,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reconnect", "Połącz ponownie"), ("Codec", "Kodek"), ("Resolution", "Rozdzielczość"), - ("Use temporary password", "Użyj hasła tymczasowego"), - ("Set temporary password length", "Ustaw długość hasła tymczasowego"), - ("Key", "Klucz"), ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 923bbab05..b62bd5a31 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Manter o serviço RustDesk em funcionamento"), ("Ignore Battery Optimizations", "Ignorar optimizações de Bateria"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Ligação não autorizada"), ("Legacy mode", ""), ("Map mode", ""), @@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), - ("No transfers in progress", ""), ("Codec", ""), ("Resolution", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index aa491f951..546ef2a3c 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Manter o serviço do RustDesk executando em segundo plano"), ("Ignore Battery Optimizations", "Ignorar otimizações de bateria"), ("android_open_battery_optimizations_tip", "Abrir otimizações de bateria"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexão não permitida"), ("Legacy mode", "Modo legado"), ("Map mode", "Modo mapa"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index e992b19d8..af9389a29 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Rulează serviciul RustDesk în fundal"), ("Ignore Battery Optimizations", "Ignoră optimizările de baterie"), ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexiune neautoriztă"), ("Legacy mode", "Mod legacy"), ("Map mode", "Mod hartă"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 3bfb5357d..b9af4ce98 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Держать в фоне службу RustDesk"), ("Ignore Battery Optimizations", "Игнорировать оптимизацию батареи"), ("android_open_battery_optimizations_tip", "Перейдите на следующую страницу настроек"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Подключение не разрешено"), ("Legacy mode", "Устаревший режим"), ("Map mode", "Режим сопоставления"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6468b7eef..8a6b765be 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index d128e7322..5721d01f4 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Povezava ni dovoljena"), ("Legacy mode", "Stari način"), ("Map mode", "Način preslikave"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 29c5cbbf8..1c488d470 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mbaje shërbimin e sfondit të RustDesk"), ("Ignore Battery Optimizations", "Injoro optimizimet e baterisë"), ("android_open_battery_optimizations_tip", "Nëse dëshironi ta çaktivizoni këtë veçori, ju lutemi shkoni te faqja tjetër e cilësimeve të aplikacionit RustDesk, gjeni dhe shtypni [Batteri], hiqni zgjedhjen [Te pakufizuara]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Lidhja nuk lejohet"), ("Legacy mode", "Modaliteti i trashëgimisë"), ("Map mode", "Modaliteti i hartës"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 63173dc11..249c0b599 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Zadrži RustDesk kao pozadinski servis"), ("Ignore Battery Optimizations", "Zanemari optimizacije baterije"), ("android_open_battery_optimizations_tip", "Ako želite da onemogućite ovu funkciju, molimo idite na sledeću stranicu za podešavanje RustDesk aplikacije, pronađite i uđite u [Battery], isključite [Unrestricted]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Konekcija nije dozvoljena"), ("Legacy mode", "Zastareli mod"), ("Map mode", "Mod mapiranja"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 1a00ece43..90ec8c1cf 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Behåll RustDesk i bakgrunden"), ("Ignore Battery Optimizations", "Ignorera batterioptimering"), ("android_open_battery_optimizations_tip", "Om du vill stänga av denna funktion, gå till nästa RustDesk programs inställningar, hitta [Batteri], Checka ur [Obegränsad]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Anslutning ej tillåten"), ("Legacy mode", "Legacy mode"), ("Map mode", "Kartläge"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 2c83f9474..6563d6056 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 6fcf02ed2..316622395 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index d35d288d6..7359bf064 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), ("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "bağlantıya izin verilmedi"), ("Legacy mode", "Eski mod"), ("Map mode", "Haritalama modu"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 20a2998ec..70533c482 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "保持RustDesk後台服務"), ("Ignore Battery Optimizations", "忽略電池優化"), ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的RustDesk應用設置頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "對方不允許連接"), ("Legacy mode", "傳統模式"), ("Map mode", "1:1傳輸"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 4c4b5d4bc..6b54c83c3 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Зберегти фонову службу RustDesk"), ("Ignore Battery Optimizations", "Ігнорувати оптимізацію батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Підключення не дозволено"), ("Legacy mode", "Застарілий режим"), ("Map mode", "Режим карти"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 32cd084cb..a379b3185 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Giữ dịch vụ nền RustDesk"), ("Ignore Battery Optimizations", "Bỏ qua các tối ưu pin"), ("android_open_battery_optimizations_tip", "Nếu bạn muốn tắt tính năng này, vui lòng chuyển đến trang cài đặt ứng dụng RustDesk tiếp theo, tìm và nhập [Pin], Bỏ chọn [Không hạn chế]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Kết nối không đuợc phép"), ("Legacy mode", ""), ("Map mode", ""), From 18339cf34322c31a66d620253da894b3db7c5560 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 28 Feb 2023 16:36:44 +0100 Subject: [PATCH 712/734] password dialog --- .../lib/desktop/widgets/remote_menubar.dart | 27 ++- flutter/lib/mobile/widgets/dialog.dart | 156 +++++++++++------- 2 files changed, 118 insertions(+), 65 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c27546d9f..90b48160b 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -655,7 +655,13 @@ class _ControlMenu extends StatelessWidget { } return CustomAlertDialog( - title: Text(translate('OS Password')), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('OS Password')).paddingOnly(left: 10), + ], + ), content: Column(mainAxisSize: MainAxisSize.min, children: [ PasswordWidget(controller: controller), CheckboxListTile( @@ -671,11 +677,22 @@ class _ControlMenu extends StatelessWidget { setState(() => autoLogin = v); }, ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + icon: Icon(Icons.close_rounded), + label: Text(translate("Cancel")), + onPressed: close, + ), + ElevatedButton.icon( + icon: Icon(Icons.done_rounded), + label: Text(translate("Ok")), + onPressed: submit, + ), + ], + ).paddingOnly(top: 20) ]), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), - ], onSubmit: submit, onCancel: close, ); diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index fde9ac4ad..6b87d62ba 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -75,64 +75,83 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { } return CustomAlertDialog( - title: Text(translate('Set your own password')), - content: Form( - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column(mainAxisSize: MainAxisSize.min, children: [ - TextFormField( - autofocus: true, - obscureText: true, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Password'), - ), - controller: p0, - validator: (v) { - if (v == null) return null; - final val = v.trim().length > 5; - if (validateLength != val) { - // use delay to make setState success - Future.delayed(Duration(microseconds: 1), - () => setState(() => validateLength = val)); - } - return val - ? null - : translate('Too short, at least 6 characters.'); - }, + title: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('Set your own password')).paddingOnly(left: 10), + ], + ), + content: Column( + children: [ + Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + autofocus: true, + obscureText: true, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + labelText: translate('Password'), + ), + controller: p0, + validator: (v) { + if (v == null) return null; + final val = v.trim().length > 5; + if (validateLength != val) { + // use delay to make setState success + Future.delayed(Duration(microseconds: 1), + () => setState(() => validateLength = val)); + } + return val + ? null + : translate('Too short, at least 6 characters.'); + }, + ), + TextFormField( + obscureText: true, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + labelText: translate('Confirmation'), + ), + controller: p1, + validator: (v) { + if (v == null) return null; + final val = p0.text == v; + if (validateSame != val) { + Future.delayed(Duration(microseconds: 1), + () => setState(() => validateSame = val)); + } + return val + ? null + : translate('The confirmation is not identical.'); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + icon: Icon(Icons.close_rounded), + label: Text(translate("Cancel")), + onPressed: close, + ), + ElevatedButton.icon( + icon: Icon(Icons.done_rounded), + label: Text(translate("Ok")), + onPressed: + (validateLength && validateSame) ? submit : null, + ), + ], + ).paddingOnly(top: 20) + ], ), - TextFormField( - obscureText: true, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Confirmation'), - ), - controller: p1, - validator: (v) { - if (v == null) return null; - final val = p0.text == v; - if (validateSame != val) { - Future.delayed(Duration(microseconds: 1), - () => setState(() => validateSame = val)); - } - return val - ? null - : translate('The confirmation is not identical.'); - }, - ), - ])), + ), + ], + ), onCancel: close, onSubmit: (validateLength && validateSame) ? submit : null, - actions: [ - dialogButton( - 'Cancel', - onPressed: close, - isOutline: true, - ), - dialogButton( - 'OK', - onPressed: (validateLength && validateSame) ? submit : null, - ), - ], ); }); } @@ -191,7 +210,13 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { } return CustomAlertDialog( - title: Text(translate('Password Required')), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('Password Required')).paddingOnly(left: 10), + ], + ), content: Column(mainAxisSize: MainAxisSize.min, children: [ PasswordWidget(controller: controller), CheckboxListTile( @@ -208,11 +233,22 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { } }, ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + icon: Icon(Icons.close_rounded), + label: Text(translate("Cancel")), + onPressed: close, + ), + ElevatedButton.icon( + icon: Icon(Icons.done_rounded), + label: Text(translate("Ok")), + onPressed: submit, + ), + ], + ).paddingOnly(top: 20) ]), - actions: [ - dialogButton('Cancel', onPressed: cancel, isOutline: true), - dialogButton('OK', onPressed: submit), - ], onSubmit: submit, onCancel: cancel, ); From e28317a8dd26a358abc4f2fe1f4035f09185b320 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 1 Mar 2023 10:12:10 +0900 Subject: [PATCH 713/734] fix lang.py --- res/lang.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/lang.py b/res/lang.py index 481d65553..aa5f99f83 100644 --- a/res/lang.py +++ b/res/lang.py @@ -36,11 +36,11 @@ def main(): def expand(): for fn in glob.glob('./src/lang/*'): lang = os.path.basename(fn)[:-3] - if lang in ['en','cn']: continue + if lang in ['en','template']: continue print(lang) dict = get_lang(lang) fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') - for line in open('./src/lang/cn.rs', encoding='utf8'): + for line in open('./src/lang/template.rs', encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) From cff2ca9df8d4d7c64731b1d27c3cd4b274b56987 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 1 Mar 2023 10:22:51 +0900 Subject: [PATCH 714/734] fix UI translate (One-time password) --- flutter/lib/mobile/pages/server_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 648448f41..bab2911f1 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -41,14 +41,14 @@ class ServerPage extends StatefulWidget implements PageShape { value: "setTemporaryPasswordLength", enabled: gFFI.serverModel.verificationMethod != kUsePermanentPassword, - child: Text(translate("Set temporary password length")), + child: Text(translate("One-time password length")), ), const PopupMenuDivider(), PopupMenuItem( padding: const EdgeInsets.symmetric(horizontal: 0.0), value: kUseTemporaryPassword, child: ListTile( - title: Text(translate("Use temporary password")), + title: Text(translate("Use one-time password")), trailing: Icon( Icons.check, color: gFFI.serverModel.verificationMethod == From 138f6fe36b3530a72fa619026dcf627308a75d26 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 1 Mar 2023 09:54:40 +0800 Subject: [PATCH 715/734] fix keyboard options, revert change Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index f236a7828..47d58ce5e 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1561,9 +1561,8 @@ class _KeyboardMenu extends StatelessWidget { @override Widget build(BuildContext context) { - // Do not check permission here? - // var ffiModel = Provider.of(context); - // if (ffiModel.permissions['keyboard'] == false) return Offstage(); + var ffiModel = Provider.of(context); + if (ffiModel.permissions['keyboard'] == false) return Offstage(); if (stateGlobal.grabKeyboard) { if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) { bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode); From 69e95bc245ab928455d6d7ab166796def6027a4d Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 1 Mar 2023 11:17:46 +0800 Subject: [PATCH 716/734] fix windows uninstall can not delete the installation directory Signed-off-by: 21pages --- src/platform/windows.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 0bba649f4..561bb4570 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -976,7 +976,7 @@ fn get_after_install(exe: &str) -> String { } pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> { - let uninstall_str = get_uninstall(); + let uninstall_str = get_uninstall(false); let mut path = path.trim_end_matches('\\').to_owned(); let (subkey, _path, start_menu, exe) = get_default_install_info(); let mut exe = exe; @@ -1188,30 +1188,35 @@ pub fn run_after_install() -> ResultType<()> { } pub fn run_before_uninstall() -> ResultType<()> { - run_cmds(get_before_uninstall(), true, "before_install") + run_cmds(get_before_uninstall(true), true, "before_install") } -fn get_before_uninstall() -> String { +fn get_before_uninstall(kill_self: bool) -> String { let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); + let filter = if kill_self { + "".to_string() + } else { + format!(" /FI \"PID ne {}\"", get_current_pid()) + }; format!( " chcp 65001 sc stop {app_name} sc delete {app_name} taskkill /F /IM {broker_exe} - taskkill /F /IM {app_name}.exe /FI \"PID ne {cur_pid}\" + taskkill /F /IM {app_name}.exe{filter} reg delete HKEY_CLASSES_ROOT\\.{ext} /f netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", app_name = app_name, broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ext = ext, - cur_pid = get_current_pid(), + filter = filter, ) } -fn get_uninstall() -> String { +fn get_uninstall(kill_self: bool) -> String { let (subkey, path, start_menu, _) = get_install_info(); format!( " @@ -1222,7 +1227,7 @@ fn get_uninstall() -> String { if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\" if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" ", - before_uninstall=get_before_uninstall(), + before_uninstall=get_before_uninstall(kill_self), subkey=subkey, app_name = crate::get_app_name(), path = path, @@ -1231,7 +1236,7 @@ fn get_uninstall() -> String { } pub fn uninstall_me() -> ResultType<()> { - run_cmds(get_uninstall(), true, "uninstall") + run_cmds(get_uninstall(true), true, "uninstall") } fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType { From 303462a87ca9261fd69429f28d0bf130dd9841f5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 1 Mar 2023 11:55:56 +0800 Subject: [PATCH 717/734] default config filed Signed-off-by: fufesou --- libs/hbb_common/examples/config.rs | 5 +++++ libs/hbb_common/src/config.rs | 26 ++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 libs/hbb_common/examples/config.rs diff --git a/libs/hbb_common/examples/config.rs b/libs/hbb_common/examples/config.rs new file mode 100644 index 000000000..95169df8e --- /dev/null +++ b/libs/hbb_common/examples/config.rs @@ -0,0 +1,5 @@ +extern crate hbb_common; + +fn main() { + println!("{:?}", hbb_common::config::PeerConfig::load("455058072")); +} diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 7bc82ed95..6fb2c895e 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -110,10 +110,10 @@ macro_rules! serde_field_string { } macro_rules! serde_field_bool { - ($struct_name: ident, $field_name: literal, $func: ident) => { + ($struct_name: ident, $field_name: literal, $func: ident, $default: literal) => { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct $struct_name { - #[serde(rename = $field_name)] + #[serde(default = $default, rename = $field_name)] pub v: bool, } impl Default for $struct_name { @@ -963,6 +963,7 @@ impl PeerConfig { }; let c = PeerConfig::load(&id_decoded_string); + log::info!("REMOVE ME ============================== peer config {:?}", &c); if c.info.platform.is_empty() { fs::remove_file(p).ok(); } @@ -1037,32 +1038,37 @@ impl PeerConfig { serde_field_bool!( ShowRemoteCursor, "show_remote_cursor", - default_show_remote_cursor + default_show_remote_cursor, + "ShowRemoteCursor::default_show_remote_cursor" ); serde_field_bool!( ShowQualityMonitor, "show_quality_monitor", - default_show_quality_monitor + default_show_quality_monitor, + "ShowQualityMonitor::default_show_quality_monitor" ); -serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio); +serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio, "DisableAudio::default_disable_audio"); serde_field_bool!( EnableFileTransfer, "enable_file_transfer", - default_enable_file_transfer + default_enable_file_transfer, + "EnableFileTransfer::default_enable_file_transfer" ); serde_field_bool!( DisableClipboard, "disable_clipboard", - default_disable_clipboard + default_disable_clipboard, + "DisableClipboard::default_disable_clipboard" ); serde_field_bool!( LockAfterSessionEnd, "lock_after_session_end", - default_lock_after_session_end + default_lock_after_session_end, + "LockAfterSessionEnd::default_lock_after_session_end" ); -serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode); +serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode, "PrivacyMode::default_privacy_mode"); -serde_field_bool!(AllowSwapKey, "allow_swap_key", default_swap_key); +serde_field_bool!(AllowSwapKey, "allow_swap_key", default_allow_swap_key, "AllowSwapKey::default_allow_swap_key"); #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LocalConfig { From 0d64ee39de95f90c915a51937b408645c728b55b Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 1 Mar 2023 12:20:54 +0800 Subject: [PATCH 718/734] remove print Signed-off-by: fufesou --- libs/hbb_common/src/config.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 6fb2c895e..ed7270a85 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -963,7 +963,6 @@ impl PeerConfig { }; let c = PeerConfig::load(&id_decoded_string); - log::info!("REMOVE ME ============================== peer config {:?}", &c); if c.info.platform.is_empty() { fs::remove_file(p).ok(); } From 874d22168b171e12829b1808cfc564d4a620aefe Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Wed, 1 Mar 2023 07:49:45 +0100 Subject: [PATCH 719/734] Update nl.rs --- src/lang/nl.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index cf3eb430c..c0e627c68 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -454,10 +454,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Spraakoproep"), ("Text chat", "Tekst chat"), ("Stop voice call", "Stop spraakoproep"), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), + ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server. \nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), + ("Reconnect", "Herverbinden"), + ("Codec", "odec"), + ("Resolution", "Resolutie"), + ("No transfers in progress", "Geen overdrachten in uitvoering"), ].iter().cloned().collect(); } From 80c39574f8c57d0e4e3bcc84a7eb2ea40fd9767a Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Wed, 1 Mar 2023 08:09:11 +0100 Subject: [PATCH 720/734] Update nl.rs --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index c0e627c68..d1c154546 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -456,7 +456,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Stop spraakoproep"), ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server. \nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), ("Reconnect", "Herverbinden"), - ("Codec", "odec"), + ("Codec", "Codec"), ("Resolution", "Resolutie"), ("No transfers in progress", "Geen overdrachten in uitvoering"), ].iter().cloned().collect(); From 2417a079aab290f07d782c90c769185d594b995f Mon Sep 17 00:00:00 2001 From: Michal Witek Date: Wed, 1 Mar 2023 09:52:26 +0100 Subject: [PATCH 721/734] Additional translation for `pl.rs` --- src/lang/pl.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index a0808f5bf..494715527 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -312,8 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"), ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"), ("android_open_battery_optimizations_tip", "Jeśli chcesz wyłączyć tę funkcję, przejdź do następnej strony ustawień aplikacji RustDesk, znajdź i wprowadź [Bateria], odznacz [Bez ograniczeń]"), - ("Start on Boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on Boot", "Autostart"), + ("Start the screen sharing service on boot, requires special permissions", "Uruchom usługę udostępniania ekranu podczas startu, wymaga specjalnych uprawnień"), ("Connection not allowed", "Połączenie niedozwolone"), ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), ("Map mode", "Tryb mapowania"), @@ -458,6 +458,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reconnect", "Połącz ponownie"), ("Codec", "Kodek"), ("Resolution", "Rozdzielczość"), - ("No transfers in progress", ""), + ("Key", "Klucz"), + ("No transfers in progress", "Brak transferów w toku"), ].iter().cloned().collect(); } From 7b80269dabcc23a57c529bed7c0e0127dfb2eb17 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 1 Mar 2023 14:18:46 +0800 Subject: [PATCH 722/734] install page use custom titlebar Signed-off-by: 21pages --- flutter/lib/desktop/pages/install_page.dart | 48 ++++++++++++++++++- .../lib/desktop/widgets/tabbar_widget.dart | 16 ++++--- flutter/lib/main.dart | 11 +++-- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index d7202e300..00ca2bb23 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -1,7 +1,9 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; @@ -13,7 +15,51 @@ class InstallPage extends StatefulWidget { State createState() => _InstallPageState(); } -class _InstallPageState extends State with WindowListener { +class _InstallPageState extends State { + final tabController = DesktopTabController(tabType: DesktopTabType.main); + + @override + void initState() { + super.initState(); + Get.put(tabController); + const lable = "install"; + tabController.add(TabInfo( + key: lable, + label: lable, + closable: false, + page: _InstallPageBody( + key: const ValueKey(lable), + ))); + } + + @override + void dispose() { + super.dispose(); + Get.delete(); + } + + @override + Widget build(BuildContext context) { + return DragToResizeArea( + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + child: Container( + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab(controller: tabController)), + ), + ); + } +} + +class _InstallPageBody extends StatefulWidget { + const _InstallPageBody({Key? key}) : super(key: key); + + @override + State<_InstallPageBody> createState() => _InstallPageBodyState(); +} + +class _InstallPageBodyState extends State<_InstallPageBody> + with WindowListener { late final TextEditingController controller; final RxBool startmenu = true.obs; final RxBool desktopicon = true.obs; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 958c4c035..edc779fba 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -53,6 +53,7 @@ enum DesktopTabType { remoteScreen, fileTransfer, portForward, + install, } class DesktopTabState { @@ -249,8 +250,9 @@ class DesktopTab extends StatelessWidget { this.unSelectedTabBackgroundColor, }) : super(key: key) { tabType = controller.tabType; - isMainWindow = - tabType == DesktopTabType.main || tabType == DesktopTabType.cm; + isMainWindow = tabType == DesktopTabType.main || + tabType == DesktopTabType.cm || + tabType == DesktopTabType.install; } static RxString labelGetterAlias(String peerId) { @@ -361,7 +363,8 @@ class DesktopTab extends StatelessWidget { /// - hide single item when only has one item (home) on [DesktopTabPage]. bool isHideSingleItem() { return state.value.tabs.length == 1 && - controller.tabType == DesktopTabType.main; + (controller.tabType == DesktopTabType.main || + controller.tabType == DesktopTabType.install); } Widget _buildBar() { @@ -524,8 +527,8 @@ class WindowActionPanelState extends State } void _setMaximize(bool maximize) { - stateGlobal.setMaximize(maximize); - setState(() {}); + stateGlobal.setMaximize(maximize); + setState(() {}); } @override @@ -759,7 +762,8 @@ class _ListView extends StatelessWidget { /// - hide single item when only has one item (home) on [DesktopTabPage]. bool isHideSingleItem() { return state.value.tabs.length == 1 && - controller.tabType == DesktopTabType.main; + controller.tabType == DesktopTabType.main || + controller.tabType == DesktopTabType.install; } @override diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6d3f863e7..bb1b4f552 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -293,16 +293,19 @@ void runInstallPage() async { await windowManager.ensureInitialized(); await initEnv(kAppTypeMain); _runApp('', const InstallPage(), MyTheme.currentThemeMode()); - windowManager.waitUntilReadyToShow( - WindowOptions(size: Size(800, 600), center: true), () async { + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(size: Size(800, 600), center: true); + windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.show(); windowManager.focus(); windowManager.setOpacity(1); windowManager.setAlignment(Alignment.center); // ensure + windowManager.setTitle(getWindowName()); }); } -WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { +WindowOptions getHiddenTitleBarWindowOptions( + {Size? size, bool center = false}) { var defaultTitleBarStyle = TitleBarStyle.hidden; // we do not hide titlebar on win7 because of the frame overflow. if (kUseCompatibleUiMode) { @@ -310,7 +313,7 @@ WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { } return WindowOptions( size: size, - center: false, + center: center, backgroundColor: Colors.transparent, skipTaskbar: false, titleBarStyle: defaultTitleBarStyle, From d1f58a444d0bf143e9658cb0b2a43f363347530f Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 1 Mar 2023 16:30:39 +0800 Subject: [PATCH 723/734] dark theme disabled button color, disabled combox color Signed-off-by: 21pages --- flutter/lib/common.dart | 21 ++++++++++++++----- .../desktop/pages/desktop_setting_page.dart | 8 +++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e0306a3a7..e049c0fe1 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -218,19 +218,30 @@ class MyTheme { labelColor: Colors.white70, ), scrollbarTheme: ScrollbarThemeData( - thumbColor: MaterialStateProperty.all(Colors.grey[500]) + thumbColor: MaterialStateProperty.all(Colors.grey[500]), ), splashColor: Colors.transparent, highlightColor: Colors.transparent, splashFactory: isDesktop ? NoSplash.splashFactory : null, outlinedButtonTheme: OutlinedButtonThemeData( - style: - OutlinedButton.styleFrom(side: BorderSide(color: Colors.white38))), + style: OutlinedButton.styleFrom( + side: BorderSide(color: Colors.white38), + disabledForegroundColor: Colors.white70, + ), + ), textButtonTheme: isDesktop ? TextButtonThemeData( - style: ButtonStyle(splashFactory: NoSplash.splashFactory), - ) + style: TextButton.styleFrom( + splashFactory: NoSplash.splashFactory, + disabledForegroundColor: Colors.white70, + )) : null, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + disabledForegroundColor: Colors.white70, + disabledBackgroundColor: Colors.white10, + ), + ), checkboxTheme: const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), colorScheme: ColorScheme.fromSwatch( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 52f64c0e3..7d907d727 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -538,6 +538,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { translate('Screen Share'), translate('Deny remote access'), ], + enabled: enabled, initialKey: initialKey, onChanged: (mode) async { String modeValue; @@ -667,6 +668,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return _Card(title: 'Password', children: [ _ComboBox( + enabled: !locked, keys: modeKeys, values: modeValues, initialKey: modeInitialKey, @@ -1722,7 +1724,6 @@ class _ComboBox extends StatelessWidget { required this.values, required this.initialKey, required this.onChanged, - // ignore: unused_element this.enabled = true, }) : super(key: key); @@ -1735,7 +1736,9 @@ class _ComboBox extends StatelessWidget { var ref = values[index].obs; current = keys[index]; return Container( - decoration: BoxDecoration(border: Border.all(color: MyTheme.border)), + decoration: BoxDecoration( + border: Border.all( + color: _disabledTextColor(context, enabled) ?? MyTheme.border)), height: 30, child: Obx(() => DropdownButton( isExpanded: true, @@ -1744,6 +1747,7 @@ class _ComboBox extends StatelessWidget { underline: Container( height: 25, ), + style: TextStyle(color: _disabledTextColor(context, enabled)), icon: const Icon( Icons.expand_more_sharp, size: 20, From 279fd7de67e5482c01a19f67057189d28edc7bae Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 1 Mar 2023 17:38:01 +0800 Subject: [PATCH 724/734] win, fix fullscreen with 3 pixels offset Signed-off-by: fufesou --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 5ffe805b8..76fe929e5 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -325,8 +325,8 @@ packages: dependency: "direct main" description: path: "." - ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a - resolved-ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a + ref: "3e2655677c54f421f9e378680d8171b95a211e0f" + resolved-ref: "3e2655677c54f421f9e378680d8171b95a211e0f" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 8d390d370..71a840c9c 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: e383fffb5c4529c9e0a710f1025a0c590b99ee08 + ref: 3e2655677c54f421f9e378680d8171b95a211e0f freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.4 window_size: From 6267d56b45e64e6b119489566274156425ee7723 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 1 Mar 2023 19:31:52 +0800 Subject: [PATCH 725/734] fix combox theme Signed-off-by: 21pages --- flutter/lib/common.dart | 8 +++++++- flutter/lib/desktop/pages/desktop_setting_page.dart | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e049c0fe1..c438a3c7b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -109,27 +109,32 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ required this.border, + required this.border2, required this.highlight, }); final Color? border; + final Color? border2; final Color? highlight; static const light = ColorThemeExtension( border: Color(0xFFCCCCCC), + border2: Color(0xFFBBBBBB), highlight: Color(0xFFE5E5E5), ); static const dark = ColorThemeExtension( border: Color(0xFF555555), + border2: Color(0xFFE5E5E5), highlight: Color(0xFF3F3F3F), ); @override ThemeExtension copyWith( - {Color? border, Color? highlight}) { + {Color? border, Color? border2, Color? highlight}) { return ColorThemeExtension( border: border ?? this.border, + border2: border2 ?? this.border2, highlight: highlight ?? this.highlight, ); } @@ -142,6 +147,7 @@ class ColorThemeExtension extends ThemeExtension { } return ColorThemeExtension( border: Color.lerp(border, other.border, t), + border2: Color.lerp(border2, other.border2, t), highlight: Color.lerp(highlight, other.highlight, t), ); } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 7d907d727..0aafd48bb 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1738,7 +1738,10 @@ class _ComboBox extends StatelessWidget { return Container( decoration: BoxDecoration( border: Border.all( - color: _disabledTextColor(context, enabled) ?? MyTheme.border)), + color: enabled + ? MyTheme.color(context).border2 ?? MyTheme.border + : MyTheme.border, + )), height: 30, child: Obx(() => DropdownButton( isExpanded: true, @@ -1747,7 +1750,10 @@ class _ComboBox extends StatelessWidget { underline: Container( height: 25, ), - style: TextStyle(color: _disabledTextColor(context, enabled)), + style: TextStyle( + color: enabled + ? Theme.of(context).textTheme.titleMedium?.color + : _disabledTextColor(context, enabled)), icon: const Icon( Icons.expand_more_sharp, size: 20, From fd8829f08e43cd2eb41c9ac9d638a9c4e7dbc4cc Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 1 Mar 2023 14:50:50 +0100 Subject: [PATCH 726/734] added icon to dialogButton, reverted some design changes. The outline buttons now rely on theme data --- flutter/lib/common.dart | 79 ++++++-- flutter/lib/common/widgets/peer_card.dart | 62 +++--- .../lib/desktop/pages/file_manager_page.dart | 47 ++--- flutter/lib/mobile/widgets/dialog.dart | 187 ++++++++---------- flutter/lib/models/file_model.dart | 39 ++-- 5 files changed, 210 insertions(+), 204 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 29d4a195d..47850fdb1 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -224,7 +224,20 @@ class MyTheme { ), shape: MaterialStateProperty.all( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Color(0xFFEEEEEE), + ), + foregroundColor: MaterialStateProperty.all(Colors.black87), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), ), ), @@ -296,9 +309,6 @@ class MyTheme { splashColor: Colors.transparent, highlightColor: Colors.transparent, splashFactory: isDesktop ? NoSplash.splashFactory : null, - outlinedButtonTheme: OutlinedButtonThemeData( - style: - OutlinedButton.styleFrom(side: BorderSide(color: Colors.white38))), textButtonTheme: isDesktop ? TextButtonThemeData( style: ButtonStyle( @@ -318,7 +328,23 @@ class MyTheme { ), shape: MaterialStateProperty.all( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll( + Color(0xFF24252B), + ), + side: MaterialStatePropertyAll( + BorderSide(color: Colors.white12, width: 0.5), + ), + foregroundColor: MaterialStateProperty.all(Colors.white70), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), ), ), @@ -1824,28 +1850,43 @@ class ServerConfig { Widget dialogButton(String text, {required VoidCallback? onPressed, bool isOutline = false, + Widget? icon, TextStyle? style, ButtonStyle? buttonStyle}) { if (isDesktop) { if (isOutline) { - return OutlinedButton( - onPressed: onPressed, - child: Text(translate(text), style: style), - ); + return icon == null + ? OutlinedButton( + onPressed: onPressed, + child: Text(translate(text), style: style), + ) + : OutlinedButton.icon( + icon: icon, + onPressed: onPressed, + label: Text(translate(text), style: style), + ); } else { - return ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), - onPressed: onPressed, - child: Text(translate(text), style: style), - ); + return icon == null + ? ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), + onPressed: onPressed, + child: Text(translate(text), style: style), + ) + : ElevatedButton.icon( + icon: icon, + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), + onPressed: onPressed, + label: Text(translate(text), style: style), + ); } } else { return TextButton( - onPressed: onPressed, - child: Text( - translate(text), - style: style, - )); + onPressed: onPressed, + child: Text( + translate(text), + style: style, + ), + ); } } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index cc5568bca..5a7f2bfa7 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -683,23 +683,21 @@ abstract class BasePeerCard extends StatelessWidget { Obx(() => Offstage( offstage: isInProgress.isFalse, child: const LinearProgressIndicator())), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton.icon( - icon: Icon(Icons.close_rounded), - label: Text(translate("Cancel")), - onPressed: close, - ), - ElevatedButton.icon( - icon: Icon(Icons.done_rounded), - label: Text(translate("Ok")), - onPressed: submit, - ), - ], - ).paddingOnly(top: 20) ], ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], onSubmit: submit, onCancel: close, ); @@ -740,26 +738,20 @@ abstract class BasePeerCard extends StatelessWidget { ), ], ), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton.icon( - icon: Icon(Icons.close_rounded), - label: Text(translate("Cancel")), - onPressed: close, - ), - ElevatedButton.icon( - icon: Icon(Icons.done_rounded), - label: Text(translate("Ok")), - onPressed: submit, - ), - ], - ).paddingOnly(top: 20) - ], - ), + content: SizedBox.shrink(), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], onSubmit: submit, onCancel: close, ); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9210a30c1..9c72caa5f 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -989,37 +989,30 @@ class _FileManagerPageState extends State content: Column( mainAxisSize: MainAxisSize.min, children: [ - Column( - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name", - ), - ), - controller: name, - autofocus: true, + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name", ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton.icon( - icon: Icon(Icons.close_rounded), - label: Text(translate("Cancel")), - onPressed: cancel, - ), - ElevatedButton.icon( - icon: Icon(Icons.done_rounded), - label: Text(translate("Ok")), - onPressed: submit, - ), - ], - ).paddingOnly(top: 20) - ], + ), + controller: name, + autofocus: true, ), ], ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "Ok", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], onSubmit: submit, onCancel: cancel, ); diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 6b87d62ba..3832ca7b1 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -28,27 +28,21 @@ void showRestartRemoteDevice( Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28), Text(translate("Restart Remote Device")).paddingOnly(left: 10), ]), - content: Column( - children: [ - Text( - "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton.icon( - icon: Icon(Icons.close_rounded), - label: Text(translate("Cancel")), - onPressed: close, - ), - ElevatedButton.icon( - icon: Icon(Icons.done_rounded), - label: Text(translate("Ok")), - onPressed: () => close(true), - ), - ], - ).paddingOnly(top: 20) - ], - ), + content: Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: () => close(true), + ), + ], onCancel: close, onSubmit: () => close(true), )); @@ -82,76 +76,65 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { Text(translate('Set your own password')).paddingOnly(left: 10), ], ), - content: Column( - children: [ - Form( - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - autofocus: true, - obscureText: true, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Password'), - ), - controller: p0, - validator: (v) { - if (v == null) return null; - final val = v.trim().length > 5; - if (validateLength != val) { - // use delay to make setState success - Future.delayed(Duration(microseconds: 1), - () => setState(() => validateLength = val)); - } - return val - ? null - : translate('Too short, at least 6 characters.'); - }, - ), - TextFormField( - obscureText: true, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Confirmation'), - ), - controller: p1, - validator: (v) { - if (v == null) return null; - final val = p0.text == v; - if (validateSame != val) { - Future.delayed(Duration(microseconds: 1), - () => setState(() => validateSame = val)); - } - return val - ? null - : translate('The confirmation is not identical.'); - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton.icon( - icon: Icon(Icons.close_rounded), - label: Text(translate("Cancel")), - onPressed: close, - ), - ElevatedButton.icon( - icon: Icon(Icons.done_rounded), - label: Text(translate("Ok")), - onPressed: - (validateLength && validateSame) ? submit : null, - ), - ], - ).paddingOnly(top: 20) - ], + content: Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + TextFormField( + autofocus: true, + obscureText: true, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + labelText: translate('Password'), + ), + controller: p0, + validator: (v) { + if (v == null) return null; + final val = v.trim().length > 5; + if (validateLength != val) { + // use delay to make setState success + Future.delayed(Duration(microseconds: 1), + () => setState(() => validateLength = val)); + } + return val + ? null + : translate('Too short, at least 6 characters.'); + }, ), - ), - ], - ), + TextFormField( + obscureText: true, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + labelText: translate('Confirmation'), + ), + controller: p1, + validator: (v) { + if (v == null) return null; + final val = p0.text == v; + if (validateSame != val) { + Future.delayed(Duration(microseconds: 1), + () => setState(() => validateSame = val)); + } + return val + ? null + : translate('The confirmation is not identical.'); + }, + ), + ])), onCancel: close, onSubmit: (validateLength && validateSame) ? submit : null, + actions: [ + dialogButton( + 'Cancel', + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: (validateLength && validateSame) ? submit : null, + ), + ], ); }); } @@ -233,22 +216,20 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { } }, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton.icon( - icon: Icon(Icons.close_rounded), - label: Text(translate("Cancel")), - onPressed: close, - ), - ElevatedButton.icon( - icon: Icon(Icons.done_rounded), - label: Text(translate("Ok")), - onPressed: submit, - ), - ], - ).paddingOnly(top: 20) ]), + actions: [ + dialogButton( + 'Cancel', + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], onSubmit: submit, onCancel: cancel, ); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 7e702f6f2..a899f4106 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -595,9 +595,10 @@ class FileModel extends ChangeNotifier { final count = entries.length > 1 ? "${i + 1}/${entries.length}" : ""; content = "$dirShow\n\n${entries[i].path}".trim(); final confirm = await showRemoveDialog( - count.isEmpty ? title : "$title ($count)", - content, - item.isDirectory); + count.isEmpty ? title : "$title ($count)", + content, + item.isDirectory, + ); try { if (confirm == true) { sendRemoveFile(entries[i].path, i, items.isLocal!); @@ -647,7 +648,7 @@ class FileModel extends ChangeNotifier { ], ), contentBoxConstraints: - BoxConstraints(minHeight: 80, minWidth: 400, maxWidth: 400), + BoxConstraints(minHeight: 100, minWidth: 400, maxWidth: 400), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -673,24 +674,22 @@ class FileModel extends ChangeNotifier { setState(() => removeCheckboxRemember = v); }, ) - : const SizedBox.shrink(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton.icon( - icon: Icon(Icons.close_rounded), - label: Text(translate("Cancel")), - onPressed: cancel, - ), - ElevatedButton.icon( - icon: Icon(Icons.done_rounded), - label: Text(translate("Ok")), - onPressed: submit, - ), - ], - ).paddingOnly(top: 20) + : const SizedBox.shrink() ], ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], onSubmit: submit, onCancel: cancel, ); From 55831948f8361742eb0473adfd0c9b1d805437f0 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 1 Mar 2023 16:35:51 +0100 Subject: [PATCH 727/734] prefere MaterialStatePropertyAll to MaterialStateProperty.all and other fixes --- flutter/lib/common.dart | 22 ++--- .../lib/desktop/widgets/remote_menubar.dart | 93 ++++++++++--------- flutter/lib/mobile/pages/server_page.dart | 2 +- flutter/lib/models/file_model.dart | 27 ++++-- 4 files changed, 82 insertions(+), 62 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 47850fdb1..eeae0972b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -209,7 +209,7 @@ class MyTheme { ? TextButtonThemeData( style: ButtonStyle( splashFactory: NoSplash.splashFactory, - shape: MaterialStateProperty.all( + shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(18.0), ), @@ -219,10 +219,10 @@ class MyTheme { : null, elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: MaterialStatePropertyAll( MyTheme.accent, ), - shape: MaterialStateProperty.all( + shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), @@ -231,11 +231,11 @@ class MyTheme { ), outlinedButtonTheme: OutlinedButtonThemeData( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: MaterialStatePropertyAll( Color(0xFFEEEEEE), ), - foregroundColor: MaterialStateProperty.all(Colors.black87), - shape: MaterialStateProperty.all( + foregroundColor: MaterialStatePropertyAll(Colors.black87), + shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), @@ -313,7 +313,7 @@ class MyTheme { ? TextButtonThemeData( style: ButtonStyle( splashFactory: NoSplash.splashFactory, - shape: MaterialStateProperty.all( + shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(18.0), ), @@ -323,10 +323,10 @@ class MyTheme { : null, elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: MaterialStatePropertyAll( MyTheme.accent, ), - shape: MaterialStateProperty.all( + shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), @@ -341,8 +341,8 @@ class MyTheme { side: MaterialStatePropertyAll( BorderSide(color: Colors.white12, width: 0.5), ), - foregroundColor: MaterialStateProperty.all(Colors.white70), - shape: MaterialStateProperty.all( + foregroundColor: MaterialStatePropertyAll(Colors.white70), + shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 90b48160b..49c56466c 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -411,17 +411,18 @@ class _RemoteMenubarState extends State { borderRadius: BorderRadius.all(Radius.circular(10)), ), child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Theme( - data: themeData(), - child: MenuBar( - children: [ - SizedBox(width: _MenubarTheme.buttonHMargin), - ...menubarItems, - SizedBox(width: _MenubarTheme.buttonHMargin) - ], - ), - )), + scrollDirection: Axis.horizontal, + child: Theme( + data: themeData(), + child: MenuBar( + children: [ + SizedBox(width: _MenubarTheme.buttonHMargin), + ...menubarItems, + SizedBox(width: _MenubarTheme.buttonHMargin) + ], + ), + ), + ), ), _buildDraggableShowHide(context), ], @@ -431,10 +432,13 @@ class _RemoteMenubarState extends State { ThemeData themeData() { return Theme.of(context).copyWith( menuButtonTheme: MenuButtonThemeData( - style: ButtonStyle( - minimumSize: MaterialStatePropertyAll(Size(64, 36)), - textStyle: MaterialStatePropertyAll( - TextStyle(fontWeight: FontWeight.normal)))), + style: ButtonStyle( + minimumSize: MaterialStatePropertyAll(Size(64, 36)), + textStyle: MaterialStatePropertyAll( + TextStyle(fontWeight: FontWeight.normal), + ), + ), + ), dividerTheme: DividerThemeData(space: 4), ); } @@ -662,37 +666,38 @@ class _ControlMenu extends StatelessWidget { Text(translate('OS Password')).paddingOnly(left: 10), ], ), - 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'), + 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); + }, ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton.icon( - icon: Icon(Icons.close_rounded), - label: Text(translate("Cancel")), - onPressed: close, - ), - ElevatedButton.icon( - icon: Icon(Icons.done_rounded), - label: Text(translate("Ok")), - onPressed: submit, - ), - ], - ).paddingOnly(top: 20) - ]), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], onSubmit: submit, onCancel: close, ); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index abccdf683..1a77b8983 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -427,7 +427,7 @@ class ConnectionManager extends StatelessWidget { ? ElevatedButton.icon( style: ButtonStyle( backgroundColor: - MaterialStateProperty.all(Colors.red)), + MaterialStatePropertyAll(Colors.red)), icon: const Icon(Icons.close), onPressed: () { bind.cmCloseConnection(connId: client.id); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index a899f4106..56c9339f3 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -708,9 +708,10 @@ class FileModel extends ChangeNotifier { return CustomAlertDialog( title: Row( children: [ - const Icon(Icons.warning, color: Colors.red), - const SizedBox(width: 20), - Text(title) + const Icon(Icons.warning_rounded, color: Colors.red), + Text(title).paddingOnly( + left: 10, + ), ], ), contentBoxConstraints: @@ -740,9 +741,23 @@ class FileModel extends ChangeNotifier { : const SizedBox.shrink() ]), actions: [ - dialogButton("Cancel", onPressed: cancel, isOutline: true), - dialogButton("Skip", onPressed: () => close(null), isOutline: true), - dialogButton("OK", onPressed: submit), + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "Skip", + icon: Icon(Icons.navigate_next_rounded), + onPressed: () => close(null), + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), ], onSubmit: submit, onCancel: cancel, From ab4ef977f411075f564bb9c5bab4885cbdd5d01c Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 1 Mar 2023 18:00:56 +0100 Subject: [PATCH 728/734] Merge branch 'master' into modern-dialog --- .github/workflows/flutter-ci.yml | 14 +- .github/workflows/flutter-nightly.yml | 6 +- Cargo.lock | 2 +- Cargo.toml | 1 + build.py | 187 ++-- docs/CONTRIBUTING-DE.md | 50 + docs/DEVCONTAINER-DE.md | 14 + docs/README-DE.md | 12 +- .../android/app/src/main/AndroidManifest.xml | 22 +- .../com/carriez/flutter_hbb/BootReceiver.kt | 34 +- .../com/carriez/flutter_hbb/MainActivity.kt | 305 +++--- .../com/carriez/flutter_hbb/MainService.kt | 58 +- .../PermissionRequestTransparentActivity.kt | 54 + .../kotlin/com/carriez/flutter_hbb/common.kt | 110 +- .../app/src/main/res/values/styles.xml | 8 + flutter/lib/common.dart | 129 ++- flutter/lib/consts.dart | 27 + .../desktop/pages/desktop_setting_page.dart | 16 +- .../lib/desktop/pages/desktop_tab_page.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 2 +- flutter/lib/desktop/pages/install_page.dart | 68 +- .../desktop/pages/port_forward_tab_page.dart | 14 +- .../lib/desktop/pages/remote_tab_page.dart | 4 +- .../desktop/screen/desktop_remote_screen.dart | 5 + .../lib/desktop/widgets/remote_menubar.dart | 38 +- .../lib/desktop/widgets/scroll_wrapper.dart | 1 + .../lib/desktop/widgets/tabbar_widget.dart | 19 +- flutter/lib/main.dart | 14 +- flutter/lib/mobile/pages/server_page.dart | 16 +- flutter/lib/mobile/pages/settings_page.dart | 120 ++- flutter/lib/models/input_model.dart | 29 +- flutter/lib/models/model.dart | 67 +- flutter/lib/models/native_model.dart | 10 +- flutter/lib/models/server_model.dart | 29 +- flutter/lib/models/state_model.dart | 10 + .../macos/Runner.xcodeproj/project.pbxproj | 2 +- flutter/pubspec.lock | 6 +- flutter/pubspec.yaml | 4 +- libs/hbb_common/examples/config.rs | 5 + libs/hbb_common/src/config.rs | 27 +- res/lang.py | 4 +- src/client.rs | 6 + src/flutter_ffi.rs | 18 +- src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 4 +- src/lang/da.rs | 2 + src/lang/de.rs | 8 +- src/lang/en.rs | 4 +- src/lang/eo.rs | 2 + src/lang/es.rs | 4 +- src/lang/fa.rs | 8 +- src/lang/fr.rs | 2 + src/lang/gr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 4 +- src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/nl.rs | 14 +- src/lang/pl.rs | 6 +- src/lang/pt_PT.rs | 4 +- src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 8 +- 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 + src/main.rs | 7 +- src/platform/macos.mm | 113 +- src/platform/macos.rs | 8 + src/platform/windows.rs | 46 +- src/server/portable_service.rs | 12 +- src/ui/header.tis | 3 +- src/ui_interface.rs | 6 +- src/ui_session_interface.rs | 96 +- vdi/host/.devcontainer/devcontainer.json | 5 +- vdi/host/Cargo.lock | 970 +++++++++++++++++- vdi/host/Cargo.toml | 4 +- 88 files changed, 2293 insertions(+), 658 deletions(-) create mode 100644 docs/CONTRIBUTING-DE.md create mode 100644 docs/DEVCONTAINER-DE.md create mode 100644 flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt create mode 100644 libs/hbb_common/examples/config.rs diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 74e4efa99..cae5b82c7 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -18,7 +18,7 @@ on: env: LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.7.0" + FLUTTER_VERSION: "3.7.5" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -260,7 +260,7 @@ jobs: job: - { target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-args: "", } steps: @@ -330,13 +330,13 @@ jobs: - { arch: x86_64, target: aarch64-linux-android, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "", } # - { # arch: x86_64, # target: armv7-linux-androideabi, - # os: ubuntu-18.04, + # os: ubuntu-20.04, # extra-build-features: "", # } steps: @@ -907,19 +907,19 @@ jobs: - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "", } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "flatpak", } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "appimage", } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b08193971..ffcadd18b 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,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features hwcodec,flutter,${{ 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,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,${{ 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,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release ;; esac diff --git a/Cargo.lock b/Cargo.lock index a2cdf91a4..8f8895bd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4656,7 +4656,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc" +source = "git+https://github.com/fufesou/rdev#25a99ce71ab42843ad253dd51e6a35e83e87a8a4" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/Cargo.toml b/Cargo.toml index f93f776a0..b53615c4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,7 @@ flutter_rust_bridge = "1.61.1" [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] +exclude = ["vdi/host"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." diff --git a/build.py b/build.py index 727b53fe0..45fe1b132 100755 --- a/build.py +++ b/build.py @@ -18,14 +18,11 @@ exe_path = 'target/release/' + hbb_name flutter_win_target_dir = 'flutter/build/windows/runner/Release/' skip_cargo = False -def custom_os_system(cmd): - err = os._system(cmd) +def system2(cmd): + err = os.system(cmd) if err != 0: print(f"Error occurred when executing: {cmd}. Exiting.") sys.exit(-1) -# replace prebuilt os.system -os._system = os.system -os.system = custom_os_system def get_version(): with open("Cargo.toml", encoding="utf-8") as fh: @@ -144,8 +141,8 @@ def generate_build_script_for_docker(): # build rustdesk ./build.py --flutter --hwcodec ''') - os.system("chmod +x /tmp/build.sh") - os.system("bash /tmp/build.sh") + system2("chmod +x /tmp/build.sh") + system2("bash /tmp/build.sh") def download_extract_features(features, res_dir): @@ -250,7 +247,7 @@ def get_features(args): def generate_control_file(version): control_file_path = "../res/DEBIAN/control" - os.system('/bin/rm -rf %s' % control_file_path) + system2('/bin/rm -rf %s' % control_file_path) content = """Package: rustdesk Version: %s @@ -268,45 +265,45 @@ Description: A remote control software. def ffi_bindgen_function_refactor(): # workaround ffigen - os.system( + system2( 'sed -i "s/ffi.NativeFunction> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") - os.system('mkdir -p tmpdeb/DEBIAN') + system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) - os.system('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') + system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - os.system('dpkg-deb -b tmpdeb rustdesk.deb;') + system2('dpkg-deb -b tmpdeb rustdesk.deb;') - os.system('/bin/rm -rf tmpdeb/') - os.system('/bin/rm -rf ../res/DEBIAN/control') + system2('/bin/rm -rf tmpdeb/') + system2('/bin/rm -rf ../res/DEBIAN/control') os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.chdir("..") @@ -314,46 +311,43 @@ def build_flutter_deb(version, features): def build_flutter_dmg(version, features): if not skip_cargo: # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project - os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') + system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') # copy dylib - os.system( + system2( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") - # ffi_bindgen_function_refactor() - # limitations from flutter rust bridge - 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( - "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") + system2('flutter build macos --release') + system2( + "create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.chdir("..") def build_flutter_arch_manjaro(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + system2(f'cargo build --features {features} --lib --release') ffi_bindgen_function_refactor() os.chdir('flutter') - os.system('flutter build linux --release') - os.system('strip build/linux/x64/release/bundle/lib/librustdesk.so') + system2('flutter build linux --release') + system2('strip build/linux/x64/release/bundle/lib/librustdesk.so') os.chdir('../res') - os.system('HBB=`pwd`/.. FLUTTER=1 makepkg -f') + system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f') def build_flutter_windows(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + system2(f'cargo build --features {features} --lib --release') if not os.path.exists("target/release/librustdesk.dll"): print("cargo build failed, please check rust source code.") exit(-1) os.chdir('flutter') - os.system('flutter build windows --release') + system2('flutter build windows --release') os.chdir('..') shutil.copy2('target/release/deps/dylib_virtual_display.dll', flutter_win_target_dir) os.chdir('libs/portable') - os.system('pip3 install -r requirements.txt') - os.system( + system2('pip3 install -r requirements.txt') + system2( f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') os.chdir('../..') if os.path.exists('./rustdesk_portable.exe'): @@ -374,22 +368,15 @@ def main(): parser = make_parser() args = parser.parse_args() - shutil.copy2('Cargo.toml', 'Cargo.toml.bk') - shutil.copy2('src/main.rs', 'src/main.rs.bk') - if windows: - txt = open('src/main.rs', encoding='utf8').read() - with open('src/main.rs', 'wt', encoding='utf8') as fh: - fh.write(txt.replace( - '//#![windows_subsystem', '#![windows_subsystem')) if os.path.exists(exe_path): os.unlink(exe_path) if os.path.isfile('/usr/bin/pacman'): - os.system('git checkout src/ui/common.tis') + system2('git checkout src/ui/common.tis') version = get_version() features = ','.join(get_features(args)) flutter = args.flutter if not flutter: - os.system('python3 res/inline-sciter.py') + system2('python3 res/inline-sciter.py') print(args.skip_cargo) if args.skip_cargo: skip_cargo = True @@ -397,55 +384,55 @@ def main(): if windows: # build virtual display dynamic library os.chdir('libs/virtual_display/dylib') - os.system('cargo build --release') + system2('cargo build --release') os.chdir('../../..') if flutter: build_flutter_windows(version, features) return - os.system('cargo build --release --features ' + features) - # os.system('upx.exe target/release/rustdesk.exe') - os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe') + system2('cargo build --release --features ' + features) + # system2('upx.exe target/release/rustdesk.exe') + system2('mv target/release/rustdesk.exe target/release/RustDesk.exe') pa = os.environ.get('P') if pa: - os.system( + system2( f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' 'target\\release\\rustdesk.exe') else: print('Not signed') - os.system( + system2( f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) + system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) if flutter: build_flutter_arch_manjaro(version, features) else: - os.system('cargo build --release --features ' + features) - os.system('git checkout src/ui/common.tis') - os.system('strip target/release/rustdesk') - os.system('ln -s res/pacman_install && ln -s res/PKGBUILD') - os.system('HBB=`pwd` makepkg -f') - os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( + system2('cargo build --release --features ' + features) + system2('git checkout src/ui/common.tis') + system2('strip target/release/rustdesk') + system2('ln -s res/pacman_install && ln -s res/PKGBUILD') + system2('HBB=`pwd` makepkg -f') + system2('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( version, version)) # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): - os.system('cargo build --release --features ' + features) - os.system('strip target/release/rustdesk') - os.system( + system2('cargo build --release --features ' + features) + system2('strip target/release/rustdesk') + system2( "sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec') - os.system( + system2('HBB=`pwd` rpmbuild -ba res/rpm.spec') + system2( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( version, version)) # yum localinstall rustdesk.rpm elif os.path.isfile('/usr/bin/zypper'): - os.system('cargo build --release --features ' + features) - os.system('strip target/release/rustdesk') - os.system( + system2('cargo build --release --features ' + features) + system2('strip target/release/rustdesk') + system2( "sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') - os.system( + system2('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') + system2( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % ( version, version)) # yum localinstall rustdesk.rpm @@ -455,18 +442,18 @@ def main(): build_flutter_dmg(version, features) pass else: - # os.system( + # system2( # 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') build_flutter_deb(version, features) else: - os.system('cargo bundle --release --features ' + features) + system2('cargo bundle --release --features ' + features) if osx: - os.system( + system2( 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') - os.system( + system2( 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') # https://github.com/sindresorhus/create-dmg - os.system('/bin/rm -rf *.dmg') + system2('/bin/rm -rf *.dmg') plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" txt = open(plist).read() with open(plist, "wt") as fh: @@ -476,7 +463,7 @@ def main(): """)) pa = os.environ.get('P') if pa: - os.system(''' + system2(''' # buggy: rcodesign sign ... path/*, have to sign one by one # install rcodesign via cargo install apple-codesign #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk @@ -486,11 +473,11 @@ def main(): codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app '''.format(pa)) - os.system('create-dmg target/release/bundle/osx/RustDesk.app') + system2('create-dmg target/release/bundle/osx/RustDesk.app') os.rename('RustDesk %s.dmg' % version, 'rustdesk-%s.dmg' % version) if pa: - os.system(''' + system2(''' # https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html # https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html # https://developer.apple.com/developer-id/ @@ -507,34 +494,32 @@ def main(): print('Not signed') else: # buid deb package - os.system( + system2( 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') - os.system('dpkg-deb -R rustdesk.deb tmpdeb') - os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( + system2('dpkg-deb -R rustdesk.deb tmpdeb') + system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + system2( 'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( + system2( 'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') - os.system( + system2( 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') - os.system( + system2( 'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') - os.system('strip tmpdeb/usr/bin/rustdesk') - os.system('mkdir -p tmpdeb/usr/lib/rustdesk') - os.system('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') - os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') + system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') + system2('strip tmpdeb/usr/bin/rustdesk') + system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') + system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/lib/rustdesk/libsciter-gtk.so') - os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) - os.system("mv Cargo.toml.bk Cargo.toml") - os.system("mv src/main.rs.bk src/main.rs") def md5_file(fn): md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest() - os.system('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) + system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) if __name__ == "__main__": diff --git a/docs/CONTRIBUTING-DE.md b/docs/CONTRIBUTING-DE.md new file mode 100644 index 000000000..6258a9a7a --- /dev/null +++ b/docs/CONTRIBUTING-DE.md @@ -0,0 +1,50 @@ +# Beitrge zu RustDesk + +RustDesk begrt Beitrge von jedem. Hier sind die Richtlinien, wenn Sie uns +helfen mchten: + +## Beitrge + +Beitrge zu RustDesk oder seinen Abhngigkeiten sollten in Form von Pull +Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur +(jemand mit der Erlaubnis, Korrekturen einzubringen) geprft und entweder in den +Hauptbaum eingefgt oder Feedback fr notwendige nderungen gegeben. Alle +Beitrge sollten diesem Format folgen, auch die von Hauptakteuren. + +Wenn Sie an einem Problem arbeiten mchten, melden Sie es bitte zuerst an, indem +Sie auf GitHub erklren, dass Sie daran arbeiten mchten. Damit soll verhindert +werden, dass Beitrge zum gleichen Thema doppelt bearbeitet werden. + +## Checkliste fr Pull Requests + +- Verzweigen Sie sich vom Master-Branch und, falls ntig, wechseln Sie zum + aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das + Zusammenfhren mit dem Master nicht reibungslos funktioniert, werden Sie + mglicherweise aufgefordert, Ihre nderungen zu berarbeiten. + +- Commits sollten so klein wie mglich sein und gleichzeitig sicherstellen, dass + jeder Commit unabhngig voneinander korrekt ist (d. h., jeder Commit sollte + sich bersetzen lassen und Tests bestehen). + +- Commits sollten von einem "Herkunftszertifikat fr Entwickler" + (https://developercertificate.org) begleitet werden, das besagt, dass Sie (und + ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE) + einverstanden sind. In Git ist dies die Option `-s` fr `git commit`. + +- Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur + Begutachtung bentigen, knnen Sie einem Gutachter mit @ antworten und um eine + Begutachtung des Pull Requests oder einen Kommentar bitten. Sie knnen auch + per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten. + +- Fgen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue + Funktion beziehen. + +Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Verhalten + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Kommunikation + +RustDesk-Mitarbeiter arbeiten hufig im [Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/DEVCONTAINER-DE.md b/docs/DEVCONTAINER-DE.md new file mode 100644 index 000000000..2a0d73f17 --- /dev/null +++ b/docs/DEVCONTAINER-DE.md @@ -0,0 +1,14 @@ + +Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Binrprogramm im Debug-Modus erstellt. + +Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an. + +Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgefhrt werden mssen, um bestimmte Builds zu erstellen. + +Kommando|Build-Typ|Modus +-|-|-| +`.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|release + diff --git a/docs/README-DE.md b/docs/README-DE.md index 8ee4a51fa..dd2aa8609 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -17,9 +17,9 @@ RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the b ![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. +RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [CONTRIBUTING-DE.md](CONTRIBUTING-DE.md) an, wenn du Unterstützung beim Start brauchst. -[**Wie arbeitet RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) @@ -41,6 +41,14 @@ Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann se | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM | | Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM | +## Dev-Container + +[![In Dev-Containern öffnen](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) + +Wenn du VS Code und Docker bereits installiert hast, kannst du auf das Abzeichen oben klicken, um loszulegen. Wenn du darauf klickst, wird VS Code automatisch die Dev-Container-Erweiterung installieren, den Quellcode in ein Container-Volume klonen und einen Dev-Container für die Verwendung aufsetzen. + +Weitere Informationen findest du in [DEVCONTAINER-DE.md](DEVCONTAINER-DE.md). + ## Abhängigkeiten Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter. diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index ede6353ef..b3c655917 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -11,22 +11,25 @@ - + + + android:supportsRtl="true"> + android:enabled="true" + android:exported="true"> + + @@ -53,8 +56,6 @@ android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize"> - - @@ -62,6 +63,11 @@ + + - + \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt index 328701567..71bbba754 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt @@ -1,21 +1,45 @@ package com.carriez.flutter_hbb +import android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +import android.Manifest.permission.SYSTEM_ALERT_WINDOW import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build +import android.util.Log import android.widget.Toast +import com.hjq.permissions.XXPermissions +import io.flutter.embedding.android.FlutterActivity + +const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED" class BootReceiver : BroadcastReceiver() { + private val logTag = "tagBootReceiver" + override fun onReceive(context: Context, intent: Intent) { - if ("android.intent.action.BOOT_COMPLETED" == intent.action){ - val it = Intent(context,MainService::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + Log.d(logTag, "onReceive ${intent.action}") + + if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) { + // check SharedPreferences config + val prefs = context.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) + if (!prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) { + Log.d(logTag, "KEY_START_ON_BOOT_OPT is false") + return } - Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show(); + // check pre-permission + if (!XXPermissions.isGranted(context, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SYSTEM_ALERT_WINDOW)){ + Log.d(logTag, "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS or SYSTEM_ALERT_WINDOW is not granted") + return + } + + val it = Intent(context, MainService::class.java).apply { + action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE + putExtra(EXT_INIT_FROM_BOOT, true) + } + Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(it) - }else{ + } else { context.startService(it) } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index fd340f7ed..52a5ff75e 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -7,35 +7,29 @@ package com.carriez.flutter_hbb * Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG */ -import android.app.Activity import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection -import android.media.projection.MediaProjectionManager import android.os.Build import android.os.IBinder -import android.provider.Settings import android.util.Log import android.view.WindowManager -import androidx.annotation.RequiresApi +import com.hjq.permissions.XXPermissions import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel -const val MEDIA_REQUEST_CODE = 42 class MainActivity : FlutterActivity() { companion object { - lateinit var flutterMethodChannel: MethodChannel + var flutterMethodChannel: MethodChannel? = null } private val channelTag = "mChannel" private val logTag = "mMainActivity" - private var mediaProjectionResultIntent: Intent? = null private var mainService: MainService? = null - @RequiresApi(Build.VERSION_CODES.M) override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) if (MainService.isReady) { @@ -46,169 +40,32 @@ class MainActivity : FlutterActivity() { flutterMethodChannel = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, channelTag - ).apply { - // make sure result is set, otherwise flutter will await forever - setMethodCallHandler { call, result -> - when (call.method) { - "init_service" -> { - Intent(activity, MainService::class.java).also { - bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) - } - if (MainService.isReady) { - result.success(false) - return@setMethodCallHandler - } - getMediaProjection() - result.success(true) - } - "start_capture" -> { - mainService?.let { - result.success(it.startCapture()) - } ?: let { - result.success(false) - } - } - "stop_service" -> { - Log.d(logTag, "Stop service") - mainService?.let { - it.destroy() - result.success(true) - } ?: let { - result.success(false) - } - } - "check_permission" -> { - if (call.arguments is String) { - result.success(checkPermission(context, call.arguments as String)) - } else { - result.success(false) - } - } - "request_permission" -> { - if (call.arguments is String) { - requestPermission(context, call.arguments as String) - result.success(true) - } else { - result.success(false) - } - } - "check_video_permission" -> { - mainService?.let { - result.success(it.checkMediaPermission()) - } ?: let { - result.success(false) - } - } - "check_service" -> { - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "input", "value" to InputService.isOpen.toString()) - ) - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "media", "value" to MainService.isReady.toString()) - ) - result.success(true) - } - "init_input" -> { - initInput() - result.success(true) - } - "stop_input" -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - InputService.ctx?.disableSelf() - } - InputService.ctx = null - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "input", "value" to InputService.isOpen.toString()) - ) - result.success(true) - } - "cancel_notification" -> { - try { - val id = call.arguments as Int - mainService?.cancelNotification(id) - } finally { - result.success(true) - } - } - "enable_soft_keyboard" -> { - // https://blog.csdn.net/hanye2020/article/details/105553780 - try { - if (call.arguments as Boolean) { - window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - } else { - window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - } - } finally { - result.success(true) - } - } - else -> { - result.error("-1", "No such method", null) - } - } - } - } - } - - private fun getMediaProjection() { - val mMediaProjectionManager = - getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager - val mIntent = mMediaProjectionManager.createScreenCaptureIntent() - startActivityForResult(mIntent, MEDIA_REQUEST_CODE) - } - - private fun initService() { - if (mediaProjectionResultIntent == null) { - Log.w(logTag, "initService fail,mediaProjectionResultIntent is null") - return - } - Log.d(logTag, "Init service") - val serviceIntent = Intent(this, MainService::class.java) - serviceIntent.action = INIT_SERVICE - serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent) - - launchMainService(serviceIntent) - } - - private fun launchMainService(intent: Intent) { - // TEST api < O - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } - } - - private fun initInput() { - val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent) - } + ) + initFlutterChannel(flutterMethodChannel!!) } override fun onResume() { super.onResume() val inputPer = InputService.isOpen activity.runOnUiThread { - flutterMethodChannel.invokeMethod( + flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "input", "value" to inputPer.toString()) ) } } + private fun requestMediaProjection() { + val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply { + action = ACT_REQUEST_MEDIA_PROJECTION + } + startActivityForResult(intent, REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION) + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == MEDIA_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK && data != null) { - mediaProjectionResultIntent = data - initService() - } else { - flutterMethodChannel.invokeMethod("on_media_projection_canceled", null) - } + if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) { + flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null) } } @@ -232,4 +89,138 @@ class MainActivity : FlutterActivity() { mainService = null } } + + private fun initFlutterChannel(flutterMethodChannel: MethodChannel) { + flutterMethodChannel.setMethodCallHandler { call, result -> + // make sure result will be invoked, otherwise flutter will await forever + when (call.method) { + "init_service" -> { + Intent(activity, MainService::class.java).also { + bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) + } + if (MainService.isReady) { + result.success(false) + return@setMethodCallHandler + } + requestMediaProjection() + result.success(true) + } + "start_capture" -> { + mainService?.let { + result.success(it.startCapture()) + } ?: let { + result.success(false) + } + } + "stop_service" -> { + Log.d(logTag, "Stop service") + mainService?.let { + it.destroy() + result.success(true) + } ?: let { + result.success(false) + } + } + "check_permission" -> { + if (call.arguments is String) { + result.success(XXPermissions.isGranted(context, call.arguments as String)) + } else { + result.success(false) + } + } + "request_permission" -> { + if (call.arguments is String) { + requestPermission(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } + START_ACTION -> { + if (call.arguments is String) { + startAction(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } + "check_video_permission" -> { + mainService?.let { + result.success(it.checkMediaPermission()) + } ?: let { + result.success(false) + } + } + "check_service" -> { + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "media", "value" to MainService.isReady.toString()) + ) + result.success(true) + } + "stop_input" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + InputService.ctx?.disableSelf() + } + InputService.ctx = null + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + result.success(true) + } + "cancel_notification" -> { + if (call.arguments is Int) { + val id = call.arguments as Int + mainService?.cancelNotification(id) + } else { + result.success(true) + } + } + "enable_soft_keyboard" -> { + // https://blog.csdn.net/hanye2020/article/details/105553780 + if (call.arguments as Boolean) { + window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + } + result.success(true) + + } + GET_START_ON_BOOT_OPT -> { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) + } + SET_START_ON_BOOT_OPT -> { + if (call.arguments is Boolean) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } + SYNC_APP_DIR_CONFIG_PATH -> { + if (call.arguments is String) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putString(KEY_APP_DIR_CONFIG_PATH, call.arguments as String) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } + else -> { + result.error("-1", "No such method", null) + } + } + } + } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index cf8e12e92..fa7440c8d 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -35,6 +35,7 @@ import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import io.flutter.embedding.android.FlutterActivity import java.util.concurrent.Executors import kotlin.concurrent.thread import org.json.JSONException @@ -43,10 +44,6 @@ import java.nio.ByteBuffer import kotlin.math.max import kotlin.math.min -const val EXTRA_MP_DATA = "mp_intent" -const val INIT_SERVICE = "init_service" -const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY" -const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY" const val DEFAULT_NOTIFY_TITLE = "RustDesk" const val DEFAULT_NOTIFY_TEXT = "Service is running" @@ -147,7 +144,11 @@ class MainService : Service() { // jvm call rust private external fun init(ctx: Context) - private external fun startServer() + + /// When app start on boot, app_dir will not be passed from flutter + /// so pass a app_dir here to rust server + private external fun startServer(app_dir: String) + private external fun startService() private external fun onVideoFrameUpdate(buf: ByteBuffer) private external fun onAudioFrameUpdate(buf: ByteBuffer) private external fun translateLocale(localeName: String, input: String): String @@ -195,6 +196,7 @@ class MainService : Service() { override fun onCreate() { super.onCreate() + Log.d(logTag,"MainService onCreate") HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply { start() serviceLooper = looper @@ -202,7 +204,13 @@ class MainService : Service() { } updateScreenInfo(resources.configuration.orientation) initNotification() - startServer() + + // keep the config dir same with flutter + val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) + val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: "" + startServer(configPath) + + createForegroundNotification() } override fun onDestroy() { @@ -277,22 +285,30 @@ class MainService : Service() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d("whichService", "this service:${Thread.currentThread()}") + Log.d("whichService", "this service: ${Thread.currentThread()}") super.onStartCommand(intent, flags, startId) - if (intent?.action == INIT_SERVICE) { - Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}") + if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) { createForegroundNotification() - val mMediaProjectionManager = + + if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) { + startService() + } + Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") + val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager - intent.getParcelableExtra(EXTRA_MP_DATA)?.let { + + intent.getParcelableExtra(EXT_MEDIA_PROJECTION_RES_INTENT)?.let { mediaProjection = - mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) + mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) checkMediaPermission() init(this) _isReady = true + } ?: let { + Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection") + requestMediaProjection() } } - return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control + return START_NOT_STICKY // don't use sticky (auto restart), the new service (from auto restart) will lose control } override fun onConfigurationChanged(newConfig: Configuration) { @@ -300,6 +316,14 @@ class MainService : Service() { updateScreenInfo(newConfig.orientation) } + private fun requestMediaProjection() { + val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply { + action = ACT_REQUEST_MEDIA_PROJECTION + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + } + @SuppressLint("WrongConstant") private fun createSurface(): Surface? { return if (useVP9) { @@ -400,13 +424,13 @@ class MainService : Service() { fun checkMediaPermission(): Boolean { Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "media", "value" to isReady.toString()) ) } Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "input", "value" to InputService.isOpen.toString()) ) @@ -653,8 +677,8 @@ class MainService : Service() { @SuppressLint("UnspecifiedImmutableFlag") private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent { val intent = Intent(this, MainService::class.java).apply { - action = ACTION_LOGIN_REQ_NOTIFY - putExtra(EXTRA_LOGIN_REQ_NOTIFY, res) + action = ACT_LOGIN_REQ_NOTIFY + putExtra(EXT_LOGIN_REQ_NOTIFY, res) } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt new file mode 100644 index 000000000..3beb7ec6b --- /dev/null +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt @@ -0,0 +1,54 @@ +package com.carriez.flutter_hbb + +import android.app.Activity +import android.content.Intent +import android.media.projection.MediaProjectionManager +import android.os.Build +import android.os.Bundle +import android.util.Log + +class PermissionRequestTransparentActivity: Activity() { + private val logTag = "permissionRequest" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(logTag, "onCreate PermissionRequestTransparentActivity: intent.action: ${intent.action}") + + when (intent.action) { + ACT_REQUEST_MEDIA_PROJECTION -> { + val mediaProjectionManager = + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + val intent = mediaProjectionManager.createScreenCaptureIntent() + startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION) + } + else -> finish() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQ_REQUEST_MEDIA_PROJECTION) { + if (resultCode == RESULT_OK && data != null) { + launchService(data) + } else { + setResult(RES_FAILED) + } + } + + finish() + } + + private fun launchService(mediaProjectionResultIntent: Intent) { + Log.d(logTag, "Launch MainService") + val serviceIntent = Intent(this, MainService::class.java) + serviceIntent.action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE + serviceIntent.putExtra(EXT_MEDIA_PROJECTION_RES_INTENT, mediaProjectionResultIntent) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } else { + startService(serviceIntent) + } + } + +} \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index 4bf244a06..f8ef07fd1 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -1,5 +1,6 @@ package com.carriez.flutter_hbb +import android.Manifest.permission.* import android.annotation.SuppressLint import android.content.Context import android.content.Intent @@ -12,8 +13,8 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.os.PowerManager -import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS -import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +import android.provider.Settings +import android.provider.Settings.* import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat.getSystemService import com.hjq.permissions.Permission @@ -22,6 +23,31 @@ import java.nio.ByteBuffer import java.util.* +// intent action, extra +const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION" +const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE" +const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" +const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT" +const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT" +const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" + +// Activity requestCode +const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101 +const val REQ_REQUEST_MEDIA_PROJECTION = 201 + +// Activity responseCode +const val RES_FAILED = -100 + +// Flutter channel +const val START_ACTION = "start_action" +const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt" +const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt" +const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir" + +const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES" +const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT" +const val KEY_APP_DIR_CONFIG_PATH = "KEY_APP_DIR_CONFIG_PATH" + @SuppressLint("ConstantLocale") val LOCAL_NAME = Locale.getDefault().toString() val SCREEN_INFO = Info(0, 0, 1, 200) @@ -30,61 +56,13 @@ data class Info( var width: Int, var height: Int, var scale: Int, var dpi: Int ) -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -fun testVP9Support(): Boolean { - return true - val res = MediaCodecList(MediaCodecList.ALL_CODECS) - .findEncoderForFormat( - MediaFormat.createVideoFormat( - MediaFormat.MIMETYPE_VIDEO_VP9, - SCREEN_INFO.width, - SCREEN_INFO.width - ) - ) - return res != null -} - -@RequiresApi(Build.VERSION_CODES.M) fun requestPermission(context: Context, type: String) { - val permission = when (type) { - "ignore_battery_optimizations" -> { - try { - context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { - data = Uri.parse("package:" + context.packageName) - }) - } catch (e:Exception) { - e.printStackTrace() - } - return - } - "application_details_settings" -> { - try { - context.startActivity(Intent().apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - action = "android.settings.APPLICATION_DETAILS_SETTINGS" - data = Uri.parse("package:" + context.packageName) - }) - } catch (e:Exception) { - e.printStackTrace() - } - return - } - "audio" -> { - Permission.RECORD_AUDIO - } - "file" -> { - Permission.MANAGE_EXTERNAL_STORAGE - } - else -> { - return - } - } XXPermissions.with(context) - .permission(permission) + .permission(type) .request { _, all -> if (all) { Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_android_permission_result", mapOf("type" to type, "result" to all) ) @@ -93,24 +71,18 @@ fun requestPermission(context: Context, type: String) { } } -@RequiresApi(Build.VERSION_CODES.M) -fun checkPermission(context: Context, type: String): Boolean { - val permission = when (type) { - "ignore_battery_optimizations" -> { - val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager - return pw.isIgnoringBatteryOptimizations(context.packageName) - } - "audio" -> { - Permission.RECORD_AUDIO - } - "file" -> { - Permission.MANAGE_EXTERNAL_STORAGE - } - else -> { - return false - } +fun startAction(context: Context, action: String) { + try { + context.startActivity(Intent(action).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // don't pass package name when launch ACTION_ACCESSIBILITY_SETTINGS + if (ACTION_ACCESSIBILITY_SETTINGS != action) { + data = Uri.parse("package:" + context.packageName) + } + }) + } catch (e: Exception) { + e.printStackTrace() } - return XXPermissions.isGranted(context, permission) } class AudioReader(val bufSize: Int, private val maxFrames: Int) { diff --git a/flutter/android/app/src/main/res/values/styles.xml b/flutter/android/app/src/main/res/values/styles.xml index d74aa35c2..146267c91 100644 --- a/flutter/android/app/src/main/res/values/styles.xml +++ b/flutter/android/app/src/main/res/values/styles.xml @@ -15,4 +15,12 @@ + diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index eeae0972b..ceff7480e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -109,27 +109,32 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ required this.border, + required this.border2, required this.highlight, }); final Color? border; + final Color? border2; final Color? highlight; static const light = ColorThemeExtension( border: Color(0xFFCCCCCC), + border2: Color(0xFFBBBBBB), highlight: Color(0xFFE5E5E5), ); static const dark = ColorThemeExtension( border: Color(0xFF555555), + border2: Color(0xFFE5E5E5), highlight: Color(0xFF3F3F3F), ); @override ThemeExtension copyWith( - {Color? border, Color? highlight}) { + {Color? border, Color? border2, Color? highlight}) { return ColorThemeExtension( border: border ?? this.border, + border2: border2 ?? this.border2, highlight: highlight ?? this.highlight, ); } @@ -142,6 +147,7 @@ class ColorThemeExtension extends ThemeExtension { } return ColorThemeExtension( border: Color.lerp(border, other.border, t), + border2: Color.lerp(border2, other.border2, t), highlight: Color.lerp(highlight, other.highlight, t), ); } @@ -207,38 +213,30 @@ class MyTheme { splashFactory: isDesktop ? NoSplash.splashFactory : null, textButtonTheme: isDesktop ? TextButtonThemeData( - style: ButtonStyle( + style: TextButton.styleFrom( splashFactory: NoSplash.splashFactory, - shape: MaterialStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), - ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), ), ), ) : null, elevatedButtonTheme: ElevatedButtonThemeData( - style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll( - MyTheme.accent, - ), - shape: MaterialStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), + style: ElevatedButton.styleFrom( + backgroundColor: MyTheme.accent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( - style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll( - Color(0xFFEEEEEE), + style: OutlinedButton.styleFrom( + backgroundColor: Color( + 0xFFEEEEEE, ), - foregroundColor: MaterialStatePropertyAll(Colors.black87), - shape: MaterialStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), + foregroundColor: Colors.black87, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), ), ), @@ -306,46 +304,42 @@ class MyTheme { tabBarTheme: const TabBarTheme( labelColor: Colors.white70, ), + scrollbarTheme: ScrollbarThemeData( + thumbColor: MaterialStateProperty.all(Colors.grey[500]), + ), splashColor: Colors.transparent, highlightColor: Colors.transparent, splashFactory: isDesktop ? NoSplash.splashFactory : null, textButtonTheme: isDesktop ? TextButtonThemeData( - style: ButtonStyle( + style: TextButton.styleFrom( splashFactory: NoSplash.splashFactory, - shape: MaterialStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), - ), + disabledForegroundColor: Colors.white70, + foregroundColor: Colors.white70, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), ), ), ) : null, elevatedButtonTheme: ElevatedButtonThemeData( - style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll( - MyTheme.accent, - ), - shape: MaterialStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), + style: ElevatedButton.styleFrom( + backgroundColor: MyTheme.accent, + disabledForegroundColor: Colors.white70, + disabledBackgroundColor: Colors.white10, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( - style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll( - Color(0xFF24252B), - ), - side: MaterialStatePropertyAll( - BorderSide(color: Colors.white12, width: 0.5), - ), - foregroundColor: MaterialStatePropertyAll(Colors.white70), - shape: MaterialStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), + style: OutlinedButton.styleFrom( + backgroundColor: Color(0xFF24252B), + side: BorderSide(color: Colors.white12, width: 0.5), + disabledForegroundColor: Colors.white70, + foregroundColor: Colors.white70, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), ), ), @@ -1045,21 +1039,14 @@ class AccessibilityListener extends StatelessWidget { } } -class PermissionManager { +class AndroidPermissionManager { static Completer? _completer; static Timer? _timer; static var _current = ""; - static final permissions = [ - "audio", - "file", - "ignore_battery_optimizations", - "application_details_settings" - ]; - static bool isWaitingFile() { if (_completer != null) { - return !_completer!.isCompleted && _current == "file"; + return !_completer!.isCompleted && _current == kManageExternalStorage; } return false; } @@ -1068,31 +1055,33 @@ class PermissionManager { if (isDesktop) { return Future.value(true); } - if (!permissions.contains(type)) { - return Future.error("Wrong permission!$type"); - } return gFFI.invokeMethod("check_permission", type); } + // startActivity goto Android Setting's page to request permission manually by user + static void startAction(String action) { + gFFI.invokeMethod(AndroidChannel.kStartAction, action); + } + + /// We use XXPermissions to request permissions, + /// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java static Future request(String type) { if (isDesktop) { return Future.value(true); } - if (!permissions.contains(type)) { - return Future.error("Wrong permission!$type"); - } gFFI.invokeMethod("request_permission", type); - if (type == "ignore_battery_optimizations") { - return Future.value(false); + + // clear last task + if (_completer?.isCompleted == false) { + _completer?.complete(false); } + _timer?.cancel(); + _current = type; _completer = Completer(); - gFFI.invokeMethod("request_permission", type); - // timeout - _timer?.cancel(); - _timer = Timer(Duration(seconds: 60), () { + _timer = Timer(Duration(seconds: 120), () { if (_completer == null) return; if (!_completer!.isCompleted) { _completer!.complete(false); @@ -1622,8 +1611,8 @@ connect(BuildContext context, String id, } } else { if (isFileTransfer) { - if (!await PermissionManager.check("file")) { - if (!await PermissionManager.request("file")) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { + if (!await AndroidPermissionManager.request(kManageExternalStorage)) { return; } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 537784918..95e4d17e7 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/state_model.dart'; const double kDesktopRemoteTabBarHeight = 28.0; const int kMainWindowId = 0; @@ -58,6 +59,12 @@ const double kDesktopFileTransferMaximumWidth = 300; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; +EdgeInsets get kDragToResizeAreaPadding => + !kUseCompatibleUiMode && Platform.isLinux + ? stateGlobal.fullscreen || stateGlobal.maximize + ? EdgeInsets.zero + : EdgeInsets.all(5.0) + : EdgeInsets.zero; // https://en.wikipedia.org/wiki/Non-breaking_space const int $nbsp = 0x00A0; @@ -79,6 +86,7 @@ const kDefaultScrollAmountMultiplier = 5.0; const kDefaultScrollDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; +const kMaximizeEdgeSize = 0.0; var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0; const kWindowBorderWidth = 1.0; const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); @@ -129,6 +137,25 @@ const kRemoteAudioDualWay = 'dual-way'; const kIgnoreDpi = true; +/// Android constants +const kActionApplicationDetailsSettings = + "android.settings.APPLICATION_DETAILS_SETTINGS"; +const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS"; + +const kRecordAudio = "android.permission.RECORD_AUDIO"; +const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE"; +const kRequestIgnoreBatteryOptimizations = + "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; +const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW"; + +/// Android channel invoke type key +class AndroidChannel { + static final kStartAction = "start_action"; + static final kGetStartOnBootOpt = "get_start_on_boot_opt"; + static final kSetStartOnBootOpt = "set_start_on_boot_opt"; + static final kSyncAppDirConfigPath = "sync_app_dir"; +} + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index e041b591d..0aafd48bb 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -19,7 +19,7 @@ import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; import '../../common/widgets/login.dart'; -const double _kTabWidth = 235; +const double _kTabWidth = 200; const double _kTabHeight = 42; const double _kCardFixedWidth = 540; const double _kCardLeftMargin = 15; @@ -538,6 +538,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { translate('Screen Share'), translate('Deny remote access'), ], + enabled: enabled, initialKey: initialKey, onChanged: (mode) async { String modeValue; @@ -667,6 +668,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return _Card(title: 'Password', children: [ _ComboBox( + enabled: !locked, keys: modeKeys, values: modeValues, initialKey: modeInitialKey, @@ -1722,7 +1724,6 @@ class _ComboBox extends StatelessWidget { required this.values, required this.initialKey, required this.onChanged, - // ignore: unused_element this.enabled = true, }) : super(key: key); @@ -1735,7 +1736,12 @@ class _ComboBox extends StatelessWidget { var ref = values[index].obs; current = keys[index]; return Container( - decoration: BoxDecoration(border: Border.all(color: MyTheme.border)), + decoration: BoxDecoration( + border: Border.all( + color: enabled + ? MyTheme.color(context).border2 ?? MyTheme.border + : MyTheme.border, + )), height: 30, child: Obx(() => DropdownButton( isExpanded: true, @@ -1744,6 +1750,10 @@ class _ComboBox extends StatelessWidget { underline: Container( height: 25, ), + style: TextStyle( + color: enabled + ? Theme.of(context).textTheme.titleMedium?.color + : _disabledTextColor(context, enabled)), icon: const Icon( Icons.expand_more_sharp, size: 20, diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 053a2d8a2..4a1a40242 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -75,7 +75,7 @@ class _DesktopTabPageState extends State { isClose: false, ), ))); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : Obx( () => DragToResizeArea( diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 148d928d9..39958e88e 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -98,7 +98,7 @@ class _FileManagerTabPageState extends State { labelGetter: DesktopTab.labelGetterAlias, )), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : SubWindowDragToResizeArea( child: tabWidget, diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index e7bb28813..00ca2bb23 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -1,7 +1,9 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; @@ -13,7 +15,51 @@ class InstallPage extends StatefulWidget { State createState() => _InstallPageState(); } -class _InstallPageState extends State with WindowListener { +class _InstallPageState extends State { + final tabController = DesktopTabController(tabType: DesktopTabType.main); + + @override + void initState() { + super.initState(); + Get.put(tabController); + const lable = "install"; + tabController.add(TabInfo( + key: lable, + label: lable, + closable: false, + page: _InstallPageBody( + key: const ValueKey(lable), + ))); + } + + @override + void dispose() { + super.dispose(); + Get.delete(); + } + + @override + Widget build(BuildContext context) { + return DragToResizeArea( + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + child: Container( + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab(controller: tabController)), + ), + ); + } +} + +class _InstallPageBody extends StatefulWidget { + const _InstallPageBody({Key? key}) : super(key: key); + + @override + State<_InstallPageBody> createState() => _InstallPageBodyState(); +} + +class _InstallPageBodyState extends State<_InstallPageBody> + with WindowListener { late final TextEditingController controller; final RxBool startmenu = true.obs; final RxBool desktopicon = true.obs; @@ -46,15 +92,19 @@ class _InstallPageState extends State with WindowListener { final double em = 13; final btnFontSize = 0.9 * em; final double button_radius = 6; + final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark; final buttonStyle = OutlinedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(button_radius)), )); final inputBorder = OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide(color: Colors.black12)); + borderSide: + BorderSide(color: isDarkTheme ? Colors.white70 : Colors.black12)); + final textColor = isDarkTheme ? null : Colors.black87; + final dividerColor = isDarkTheme ? Colors.white70 : Colors.black87; return Scaffold( - backgroundColor: Colors.white, + backgroundColor: null, body: SingleChildScrollView( child: Column( children: [ @@ -91,8 +141,7 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Change Path'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(left: em)) ], ).marginSymmetric(vertical: 2 * em), @@ -127,8 +176,7 @@ class _InstallPageState extends State with WindowListener { )).marginOnly(top: 2 * em), Row(children: [Text(translate('agreement_tip'))]) .marginOnly(top: em), - Divider(color: Colors.black87) - .marginSymmetric(vertical: 0.5 * em), + Divider(color: dividerColor).marginSymmetric(vertical: 0.5 * em), Row( children: [ Expanded( @@ -143,8 +191,7 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Cancel'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(right: 2 * em)), Obx(() => ElevatedButton( onPressed: btnEnabled.value ? install : null, @@ -167,8 +214,7 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Run without install'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(left: 2 * em)), ), ], diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index f2d75d00f..32f02c9b7 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -107,13 +107,15 @@ class _PortForwardTabPageState extends State { labelGetter: DesktopTab.labelGetterAlias, )), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget - : SubWindowDragToResizeArea( - child: tabWidget, - resizeEdgeSize: stateGlobal.resizeEdgeSize.value, - windowId: stateGlobal.windowId, - ); + : Obx( + () => SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + ), + ); } void onRemoveId(String id) { diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 0deb646c0..d810650fd 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -205,11 +205,13 @@ class _ConnectionTabPageState extends State { ), ), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : Obx(() => SubWindowDragToResizeArea( key: contentKey, child: tabWidget, + // Specially configured for a better resize area and remote control. + childPadding: kDragToResizeAreaPadding, resizeEdgeSize: stateGlobal.resizeEdgeSize.value, windowId: stateGlobal.windowId, )); diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index bb6bc431b..64af41401 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; @@ -26,6 +28,9 @@ class DesktopRemoteScreen extends StatelessWidget { ChangeNotifierProvider.value(value: gFFI.canvasModel), ], child: Scaffold( + // Set transparent background for padding the resize area out of the flutter view. + // This allows the wallpaper goes through our resize area. (Linux only now). + backgroundColor: Platform.isLinux ? Colors.transparent : null, body: ConnectionTabPage( params: params, ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 49c56466c..081cd1649 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -942,6 +942,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { disableClipboard(), lockAfterSessionEnd(), privacyMode(), + swapKey(), ]); } @@ -975,12 +976,13 @@ class _DisplayMenuState extends State<_DisplayMenu> { final canvasModel = widget.ffi.canvasModel; final width = (canvasModel.getDisplayWidth() * canvasModel.scale + - canvasModel.windowBorderWidth * 2) * + CanvasModel.leftToEdge + + CanvasModel.rightToEdge) * scale + magicWidth; final height = (canvasModel.getDisplayHeight() * canvasModel.scale + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * + CanvasModel.topToEdge + + CanvasModel.bottomToEdge) * scale + magicHeight; double left = wndRect.left + (wndRect.width - width) / 2; @@ -1049,10 +1051,10 @@ class _DisplayMenuState extends State<_DisplayMenu> { final canvasModel = widget.ffi.canvasModel; final displayWidth = canvasModel.getDisplayWidth(); final displayHeight = canvasModel.getDisplayHeight(); - final requiredWidth = displayWidth + - (canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2); - final requiredHeight = displayHeight + - (canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2); + final requiredWidth = + CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge; + final requiredHeight = + CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge; return selfWidth > (requiredWidth * scale) && selfHeight > (requiredHeight * scale); } @@ -1549,6 +1551,23 @@ class _DisplayMenuState extends State<_DisplayMenu> { ffi: widget.ffi, child: Text(translate('Privacy mode'))); } + + swapKey() { + final visible = perms['keyboard'] != false && + ((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) || + (!Platform.isMacOS && pi.platform == kPeerPlatformMacOS)); + if (!visible) return Offstage(); + final option = 'allow_swap_key'; + 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('Swap control-command key'))); + } } class _KeyboardMenu extends StatelessWidget { @@ -1564,9 +1583,8 @@ class _KeyboardMenu extends StatelessWidget { @override Widget build(BuildContext context) { - // Do not check permission here? - // var ffiModel = Provider.of(context); - // if (ffiModel.permissions['keyboard'] == false) return Offstage(); + var ffiModel = Provider.of(context); + if (ffiModel.permissions['keyboard'] == false) return Offstage(); if (stateGlobal.grabKeyboard) { if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) { bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode); diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart index 32ed149e5..c5bc3394b 100644 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ b/flutter/lib/desktop/widgets/scroll_wrapper.dart @@ -14,6 +14,7 @@ class DesktopScrollWrapper extends StatelessWidget { return ImprovedScrolling( scrollController: scrollController, enableCustomMouseWheelScrolling: true, + // enableKeyboardScrolling: true, // strange behavior on mac customMouseWheelScrollConfig: CustomMouseWheelScrollConfig( scrollDuration: kDefaultScrollDuration, scrollCurve: Curves.linearToEaseOut, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index ee3aaaf2c..edc779fba 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -53,6 +53,7 @@ enum DesktopTabType { remoteScreen, fileTransfer, portForward, + install, } class DesktopTabState { @@ -249,8 +250,9 @@ class DesktopTab extends StatelessWidget { this.unSelectedTabBackgroundColor, }) : super(key: key) { tabType = controller.tabType; - isMainWindow = - tabType == DesktopTabType.main || tabType == DesktopTabType.cm; + isMainWindow = tabType == DesktopTabType.main || + tabType == DesktopTabType.cm || + tabType == DesktopTabType.install; } static RxString labelGetterAlias(String peerId) { @@ -361,7 +363,8 @@ class DesktopTab extends StatelessWidget { /// - hide single item when only has one item (home) on [DesktopTabPage]. bool isHideSingleItem() { return state.value.tabs.length == 1 && - controller.tabType == DesktopTabType.main; + (controller.tabType == DesktopTabType.main || + controller.tabType == DesktopTabType.install); } Widget _buildBar() { @@ -523,12 +526,18 @@ class WindowActionPanelState extends State super.dispose(); } + void _setMaximize(bool maximize) { + stateGlobal.setMaximize(maximize); + setState(() {}); + } + @override void onWindowMaximize() { // catch maximize from system if (!widget.isMaximized.value) { widget.isMaximized.value = true; } + _setMaximize(true); super.onWindowMaximize(); } @@ -538,6 +547,7 @@ class WindowActionPanelState extends State if (widget.isMaximized.value) { widget.isMaximized.value = false; } + _setMaximize(false); super.onWindowUnmaximize(); } @@ -752,7 +762,8 @@ class _ListView extends StatelessWidget { /// - hide single item when only has one item (home) on [DesktopTabPage]. bool isHideSingleItem() { return state.value.tabs.length == 1 && - controller.tabType == DesktopTabType.main; + controller.tabType == DesktopTabType.main || + controller.tabType == DesktopTabType.install; } @override diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c61287d4f..bb1b4f552 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -153,6 +153,7 @@ void runMainApp(bool startService) async { void runMobileApp() async { await initEnv(kAppTypeMain); if (isAndroid) androidChannelInit(); + platformFFI.syncAndroidServiceAppDirConfigPath(); runApp(App()); } @@ -291,17 +292,20 @@ void _runApp( void runInstallPage() async { await windowManager.ensureInitialized(); await initEnv(kAppTypeMain); - _runApp('', const InstallPage(), ThemeMode.light); - windowManager.waitUntilReadyToShow( - WindowOptions(size: Size(800, 600), center: true), () async { + _runApp('', const InstallPage(), MyTheme.currentThemeMode()); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(size: Size(800, 600), center: true); + windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.show(); windowManager.focus(); windowManager.setOpacity(1); windowManager.setAlignment(Alignment.center); // ensure + windowManager.setTitle(getWindowName()); }); } -WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { +WindowOptions getHiddenTitleBarWindowOptions( + {Size? size, bool center = false}) { var defaultTitleBarStyle = TitleBarStyle.hidden; // we do not hide titlebar on win7 because of the frame overflow. if (kUseCompatibleUiMode) { @@ -309,7 +313,7 @@ WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { } return WindowOptions( size: size, - center: false, + center: center, backgroundColor: Colors.transparent, skipTaskbar: false, titleBarStyle: defaultTitleBarStyle, diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 1a77b8983..218559a6e 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; +import '../../consts.dart'; import '../../models/platform_model.dart'; import '../../models/server_model.dart'; import 'home_page.dart'; @@ -40,14 +41,14 @@ class ServerPage extends StatefulWidget implements PageShape { value: "setTemporaryPasswordLength", enabled: gFFI.serverModel.verificationMethod != kUsePermanentPassword, - child: Text(translate("Set temporary password length")), + child: Text(translate("One-time password length")), ), const PopupMenuDivider(), PopupMenuItem( padding: const EdgeInsets.symmetric(horizontal: 0.0), value: kUseTemporaryPassword, child: ListTile( - title: Text(translate("Use temporary password")), + title: Text(translate("Use one-time password")), trailing: Icon( Icons.check, color: gFFI.serverModel.verificationMethod == @@ -150,10 +151,11 @@ class _ServerPageState extends State { } void checkService() async { - gFFI.invokeMethod("check_service"); // jvm - // for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page - if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { - PermissionManager.complete("file", await PermissionManager.check("file")); + gFFI.invokeMethod("check_service"); + // for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page + if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { + AndroidPermissionManager.complete(kManageExternalStorage, + await AndroidPermissionManager.check(kManageExternalStorage)); debugPrint("file permission finished"); } } @@ -567,7 +569,7 @@ void androidChannelInit() { { var type = arguments["type"] as String; var result = arguments["result"] as bool; - PermissionManager.complete(type, result); + AndroidPermissionManager.complete(type, result); break; } case "on_media_projection_canceled": diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index c5f3b6935..e07f8f59f 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; import '../../common/widgets/login.dart'; +import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -31,18 +32,20 @@ class SettingsPage extends StatefulWidget implements PageShape { } const url = 'https://rustdesk.com/'; -final _hasIgnoreBattery = androidVersion >= 26; -var _ignoreBatteryOpt = false; -var _enableAbr = false; -var _denyLANDiscovery = false; -var _onlyWhiteList = false; -var _enableDirectIPAccess = false; -var _enableRecordSession = false; -var _autoRecordIncomingSession = false; -var _localIP = ""; -var _directAccessPort = ""; class _SettingsState extends State with WidgetsBindingObserver { + final _hasIgnoreBattery = androidVersion >= 26; + var _ignoreBatteryOpt = false; + var _enableStartOnBoot = false; + var _enableAbr = false; + var _denyLANDiscovery = false; + var _onlyWhiteList = false; + var _enableDirectIPAccess = false; + var _enableRecordSession = false; + var _autoRecordIncomingSession = false; + var _localIP = ""; + var _directAccessPort = ""; + @override void initState() { super.initState(); @@ -50,11 +53,34 @@ class _SettingsState extends State with WidgetsBindingObserver { () async { var update = false; + if (_hasIgnoreBattery) { - update = await updateIgnoreBatteryStatus(); + if (await checkAndUpdateIgnoreBatteryStatus()) { + update = true; + } } - final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N"; + if (await checkAndUpdateStartOnBoot()) { + update = true; + } + + // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW + var enableStartOnBoot = + await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt); + if (enableStartOnBoot) { + if (!await canStartOnBoot()) { + enableStartOnBoot = false; + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); + } + } + + if (enableStartOnBoot != _enableStartOnBoot) { + update = true; + _enableStartOnBoot = enableStartOnBoot; + } + + final enableAbrRes = option2bool( + "enable-abr", await bind.mainGetOption(key: "enable-abr")); if (enableAbrRes != _enableAbr) { update = true; _enableAbr = enableAbrRes; @@ -125,15 +151,18 @@ class _SettingsState extends State with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { () async { - if (await updateIgnoreBatteryStatus()) { + final ibs = await checkAndUpdateIgnoreBatteryStatus(); + final sob = await checkAndUpdateStartOnBoot(); + if (ibs || sob) { setState(() {}); } }(); } } - Future updateIgnoreBatteryStatus() async { - final res = await PermissionManager.check("ignore_battery_optimizations"); + Future checkAndUpdateIgnoreBatteryStatus() async { + final res = await AndroidPermissionManager.check( + kRequestIgnoreBatteryOptimizations); if (_ignoreBatteryOpt != res) { _ignoreBatteryOpt = res; return true; @@ -142,6 +171,18 @@ class _SettingsState extends State with WidgetsBindingObserver { } } + Future checkAndUpdateStartOnBoot() async { + if (!await canStartOnBoot() && _enableStartOnBoot) { + _enableStartOnBoot = false; + debugPrint( + "checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false"); + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); + return true; + } else { + return false; + } + } + @override Widget build(BuildContext context) { Provider.of(context); @@ -265,7 +306,8 @@ class _SettingsState extends State with WidgetsBindingObserver { ]), onToggle: (v) async { if (v) { - PermissionManager.request("ignore_battery_optimizations"); + await AndroidPermissionManager.request( + kRequestIgnoreBatteryOptimizations); } else { final res = await gFFI.dialogManager .show((setState, close) => CustomAlertDialog( @@ -282,11 +324,44 @@ class _SettingsState extends State with WidgetsBindingObserver { ], )); if (res == true) { - PermissionManager.request("application_details_settings"); + AndroidPermissionManager.startAction( + kActionApplicationDetailsSettings); } } })); } + enhancementsTiles.add(SettingsTile.switchTile( + initialValue: _enableStartOnBoot, + title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text("${translate('Start on Boot')} (beta)"), + Text( + '* ${translate('Start the screen sharing service on boot, requires special permissions')}', + style: Theme.of(context).textTheme.bodySmall), + ]), + onToggle: (toValue) async { + if (toValue) { + // 1. request kIgnoreBatteryOptimizations + if (!await AndroidPermissionManager.check( + kRequestIgnoreBatteryOptimizations)) { + if (!await AndroidPermissionManager.request( + kRequestIgnoreBatteryOptimizations)) { + return; + } + } + + // 2. request kSystemAlertWindow + if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { + if (!await AndroidPermissionManager.request(kSystemAlertWindow)) { + return; + } + } + + // (Optional) 3. request input permission + } + setState(() => _enableStartOnBoot = toValue); + + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue); + })); return SettingsList( sections: [ @@ -387,6 +462,17 @@ class _SettingsState extends State with WidgetsBindingObserver { ], ); } + + Future canStartOnBoot() async { + // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW + if (_hasIgnoreBattery && !_ignoreBatteryOpt) { + return false; + } + if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { + return false; + } + return true; + } } void showServerSettings(OverlayDialogManager dialogManager) async { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index fca73eac7..df9ad2585 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -458,10 +458,8 @@ class InputModel { return; } evt['type'] = type; - if (isDesktop) { - y = y - stateGlobal.tabBarHeight - stateGlobal.windowBorderWidth.value; - x -= stateGlobal.windowBorderWidth.value; - } + y -= CanvasModel.topToEdge; + x -= CanvasModel.leftToEdge; final canvasModel = parent.target!.canvasModel; final nearThr = 3; var nearRight = (canvasModel.size.width - x) < nearThr; @@ -503,8 +501,21 @@ class InputModel { } x += d.x; y += d.y; + var evtX = 0; + var evtY = 0; + try { + evtX = x.round(); + evtY = y.round(); + } catch (e) { + debugPrintStack( + label: 'canvasModel.scale value ${canvasModel.scale}, $e'); + return; + } - if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) { + if (evtX < d.x || + evtY < d.y || + evtX > (d.x + d.width) || + evtY > (d.y + d.height)) { // If left mouse up, no early return. if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { return; @@ -512,12 +523,12 @@ class InputModel { } if (type != '') { - x = 0; - y = 0; + evtX = 0; + evtY = 0; } - evt['x'] = '${x.round()}'; - evt['y'] = '${y.round()}'; + evt['x'] = '$evtX'; + evt['y'] = '$evtY'; var buttons = ''; switch (evt['buttons']) { case kPrimaryMouseButton: diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e48d74dac..802a18a52 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -617,13 +617,28 @@ class ViewStyle { final int displayWidth; final int displayHeight; ViewStyle({ - this.style = '', - this.width = 0.0, - this.height = 0.0, - this.displayWidth = 0, - this.displayHeight = 0, + required this.style, + required this.width, + required this.height, + required this.displayWidth, + required this.displayHeight, }); + static defaultViewStyle() { + final desktop = (isDesktop || isWebDesktop); + final w = + desktop ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth; + final h = + desktop ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; + return ViewStyle( + style: '', + width: w.toDouble(), + height: h.toDouble(), + displayWidth: w, + displayHeight: h, + ); + } + static int _double2Int(double v) => (v * 100).round().toInt(); @override @@ -652,9 +667,14 @@ class ViewStyle { double get scale { double s = 1.0; if (style == kRemoteViewStyleAdaptive) { - final s1 = width / displayWidth; - final s2 = height / displayHeight; - s = s1 < s2 ? s1 : s2; + if (width != 0 && + height != 0 && + displayWidth != 0 && + displayHeight != 0) { + final s1 = width / displayWidth; + final s2 = height / displayHeight; + s = s1 < s2 ? s1 : s2; + } } return s; } @@ -680,7 +700,7 @@ class CanvasModel with ChangeNotifier { // scroll offset y percent double _scrollY = 0.0; ScrollStyle _scrollStyle = ScrollStyle.scrollauto; - ViewStyle _lastViewStyle = ViewStyle(); + ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle(); final _imageOverflow = false.obs; @@ -707,12 +727,25 @@ class CanvasModel with ChangeNotifier { double get scrollX => _scrollX; double get scrollY => _scrollY; + static double get leftToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.left + : 0; + static double get rightToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.right + : 0; + static double get topToEdge => (isDesktop || isWebDesktop) + ? tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top + : 0; + static double get bottomToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.bottom + : 0; + updateViewStyle() async { Size getSize() { final size = MediaQueryData.fromWindow(ui.window).size; // If minimized, w or h may be negative here. - double w = size.width - windowBorderWidth * 2; - double h = size.height - tabBarHeight - windowBorderWidth * 2; + double w = size.width - leftToEdge - rightToEdge; + double h = size.height - topToEdge - bottomToEdge; return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); } @@ -786,10 +819,14 @@ class CanvasModel with ChangeNotifier { return parent.target?.ffiModel.display.height ?? defaultHeight; } - double get windowBorderWidth => stateGlobal.windowBorderWidth.value; - double get tabBarHeight => stateGlobal.tabBarHeight; + static double get windowBorderWidth => stateGlobal.windowBorderWidth.value; + static double get tabBarHeight => stateGlobal.tabBarHeight; moveDesktopMouse(double x, double y) { + if (size.width == 0 || size.height == 0) { + return; + } + // On mobile platforms, move the canvas with the cursor. final dw = getDisplayWidth() * _scale; final dh = getDisplayHeight() * _scale; @@ -803,7 +840,9 @@ class CanvasModel with ChangeNotifier { dyOffset = (y - dh * (y / size.height) - _y).toInt(); } } catch (e) { - // Unhandled Exception: Unsupported operation: Infinity or NaN toInt + debugPrintStack( + label: + '(x,y) ($x,$y), (_x,_y) ($_x,$_y), _scale $_scale, display size (${getDisplayWidth()},${getDisplayHeight()}), size $size, , $e'); return; } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 13f5b4587..28dc8085e 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,7 +30,7 @@ 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) +// pub fn session_register_texture(id: *const char, ptr: usize) typedef F6 = Void Function(Pointer, Uint64); typedef F6Dart = void Function(Pointer, int); @@ -56,7 +56,6 @@ class PlatformFFI { F4Dart? _session_get_rgba_size; F5Dart? _session_next_rgba; F6Dart? _session_register_texture; - static get localeName => Platform.localeName; @@ -162,7 +161,8 @@ class PlatformFFI { dylib.lookupFunction("session_get_rgba_size"); _session_next_rgba = dylib.lookupFunction("session_next_rgba"); - _session_register_texture = dylib.lookupFunction("session_register_texture"); + _session_register_texture = + dylib.lookupFunction("session_register_texture"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; @@ -301,4 +301,8 @@ class PlatformFFI { if (!isAndroid) return Future(() => false); return await _toAndroidChannel.invokeMethod(method, arguments); } + + void syncAndroidServiceAppDirConfigPath() { + invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir); + } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index b2043f3c2..7ee23ec40 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; @@ -154,7 +155,8 @@ class ServerModel with ChangeNotifier { /// file true by default (if permission on) checkAndroidPermission() async { // audio - if (androidVersion < 30 || !await PermissionManager.check("audio")) { + if (androidVersion < 30 || + !await AndroidPermissionManager.check(kRecordAudio)) { _audioOk = false; bind.mainSetOption(key: "enable-audio", value: "N"); } else { @@ -163,7 +165,7 @@ class ServerModel with ChangeNotifier { } // file - if (!await PermissionManager.check("file")) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { _fileOk = false; bind.mainSetOption(key: "enable-file-transfer", value: "N"); } else { @@ -229,10 +231,10 @@ class ServerModel with ChangeNotifier { } toggleAudio() async { - if (!_audioOk && !await PermissionManager.check("audio")) { - final res = await PermissionManager.request("audio"); + if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) { + final res = await AndroidPermissionManager.request(kRecordAudio); if (!res) { - // TODO handle fail + showToast(translate('Failed')); return; } } @@ -243,10 +245,12 @@ class ServerModel with ChangeNotifier { } toggleFile() async { - if (!_fileOk && !await PermissionManager.check("file")) { - final res = await PermissionManager.request("file"); + if (!_fileOk && + !await AndroidPermissionManager.check(kManageExternalStorage)) { + final res = + await AndroidPermissionManager.request(kManageExternalStorage); if (!res) { - // TODO handle fail + showToast(translate('Failed')); return; } } @@ -344,10 +348,6 @@ class ServerModel with ChangeNotifier { } } - Future initInput() async { - await parent.target?.invokeMethod("init_input"); - } - Future setPermanentPassword(String newPW) async { await bind.mainSetPermanentPassword(password: newPW); await Future.delayed(Duration(milliseconds: 500)); @@ -561,7 +561,8 @@ class ServerModel with ChangeNotifier { } Future closeAll() async { - await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id))); + await Future.wait( + _clients.map((client) => bind.cmCloseConnection(connId: client.id))); _clients.clear(); tabController.state.value.tabs.clear(); } @@ -684,7 +685,7 @@ String getLoginDialogTag(int id) { showInputWarnAlert(FFI ffi) { ffi.dialogManager.show((setState, close) { submit() { - ffi.serverModel.initInput(); + AndroidPermissionManager.startAction(kActionAccessibilitySettings); close(); } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 761c95ded..aa4fab86e 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -9,8 +9,10 @@ import '../consts.dart'; class StateGlobal { int _windowId = -1; bool _fullscreen = false; + bool _maximize = false; bool grabKeyboard = false; final RxBool _showTabBar = true.obs; + final RxBool _showResizeEdge = true.obs; final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; @@ -18,12 +20,20 @@ class StateGlobal { int get windowId => _windowId; bool get fullscreen => _fullscreen; + bool get maximize => _maximize; double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight; RxBool get showTabBar => _showTabBar; RxDouble get resizeEdgeSize => _resizeEdgeSize; RxDouble get windowBorderWidth => _windowBorderWidth; setWindowId(int id) => _windowId = id; + setMaximize(bool v) { + if (_maximize != v) { + _maximize = v; + _resizeEdgeSize.value = + _maximize ? kMaximizeEdgeSize : kWindowEdgeSize; + } + } setFullscreen(bool v) { if (_fullscreen != v) { _fullscreen = v; diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 0019335ef..c73e666c7 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -487,7 +487,7 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 5ffe805b8..2202b2ccf 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -325,8 +325,8 @@ packages: dependency: "direct main" description: path: "." - ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a - resolved-ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a + ref: "3e2655677c54f421f9e378680d8171b95a211e0f" + resolved-ref: "3e2655677c54f421f9e378680d8171b95a211e0f" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -1563,5 +1563,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 572b3e20a..71a840c9c 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: f37357ed98a10717576eb9ed8413e92b2ec5d13a + ref: 3e2655677c54f421f9e378680d8171b95a211e0f freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.4 window_size: @@ -76,7 +76,7 @@ dependencies: 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. + # currently, we use flutter 3.7.0+. # # 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). diff --git a/libs/hbb_common/examples/config.rs b/libs/hbb_common/examples/config.rs new file mode 100644 index 000000000..95169df8e --- /dev/null +++ b/libs/hbb_common/examples/config.rs @@ -0,0 +1,5 @@ +extern crate hbb_common; + +fn main() { + println!("{:?}", hbb_common::config::PeerConfig::load("455058072")); +} diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 3bfc885c5..ed7270a85 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -110,10 +110,10 @@ macro_rules! serde_field_string { } macro_rules! serde_field_bool { - ($struct_name: ident, $field_name: literal, $func: ident) => { + ($struct_name: ident, $field_name: literal, $func: ident, $default: literal) => { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct $struct_name { - #[serde(rename = $field_name)] + #[serde(default = $default, rename = $field_name)] pub v: bool, } impl Default for $struct_name { @@ -217,6 +217,8 @@ pub struct PeerConfig { pub lock_after_session_end: LockAfterSessionEnd, #[serde(flatten)] pub privacy_mode: PrivacyMode, + #[serde(flatten)] + pub allow_swap_key: AllowSwapKey, #[serde(default)] pub port_forwards: Vec<(i32, String, i32)>, #[serde(default)] @@ -1035,30 +1037,37 @@ impl PeerConfig { serde_field_bool!( ShowRemoteCursor, "show_remote_cursor", - default_show_remote_cursor + default_show_remote_cursor, + "ShowRemoteCursor::default_show_remote_cursor" ); serde_field_bool!( ShowQualityMonitor, "show_quality_monitor", - default_show_quality_monitor + default_show_quality_monitor, + "ShowQualityMonitor::default_show_quality_monitor" ); -serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio); +serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio, "DisableAudio::default_disable_audio"); serde_field_bool!( EnableFileTransfer, "enable_file_transfer", - default_enable_file_transfer + default_enable_file_transfer, + "EnableFileTransfer::default_enable_file_transfer" ); serde_field_bool!( DisableClipboard, "disable_clipboard", - default_disable_clipboard + default_disable_clipboard, + "DisableClipboard::default_disable_clipboard" ); serde_field_bool!( LockAfterSessionEnd, "lock_after_session_end", - default_lock_after_session_end + default_lock_after_session_end, + "LockAfterSessionEnd::default_lock_after_session_end" ); -serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode); +serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode, "PrivacyMode::default_privacy_mode"); + +serde_field_bool!(AllowSwapKey, "allow_swap_key", default_allow_swap_key, "AllowSwapKey::default_allow_swap_key"); #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LocalConfig { diff --git a/res/lang.py b/res/lang.py index 481d65553..aa5f99f83 100644 --- a/res/lang.py +++ b/res/lang.py @@ -36,11 +36,11 @@ def main(): def expand(): for fn in glob.glob('./src/lang/*'): lang = os.path.basename(fn)[:-3] - if lang in ['en','cn']: continue + if lang in ['en','template']: continue print(lang) dict = get_lang(lang) fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') - for line in open('./src/lang/cn.rs', encoding='utf8'): + for line in open('./src/lang/template.rs', encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) diff --git a/src/client.rs b/src/client.rs index ebfda7283..40a9f05b0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1230,6 +1230,8 @@ impl LoginConfigHandler { option.block_input = BoolOption::No.into(); } else if name == "show-quality-monitor" { config.show_quality_monitor.v = !config.show_quality_monitor.v; + } else if name == "allow_swap_key" { + config.allow_swap_key.v = !config.allow_swap_key.v; } else { let is_set = self .options @@ -1383,6 +1385,8 @@ impl LoginConfigHandler { self.config.disable_clipboard.v } else if name == "show-quality-monitor" { self.config.show_quality_monitor.v + } else if name == "allow_swap_key" { + self.config.allow_swap_key.v } else { !self.get_option(name).is_empty() } @@ -1807,6 +1811,7 @@ pub fn send_mouse( if check_scroll_on_mac(mask, x, y) { mouse_event.modifiers.push(ControlKey::Scroll.into()); } + interface.swap_modifier_mouse(&mut mouse_event); msg_out.set_mouse_event(mouse_event); interface.send(Data::Message(msg_out)); } @@ -2033,6 +2038,7 @@ pub trait Interface: Send + Clone + 'static + Sized { fn is_force_relay(&self) -> bool { self.get_login_config_handler().read().unwrap().force_relay } + fn swap_modifier_mouse(&self, _msg : &mut hbb_common::protos::message::MouseEvent) {} } /// Data used by the client interface. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e49ba65f7..e5b24fa53 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1361,7 +1361,7 @@ pub fn send_url_scheme(_url: String) { #[cfg(target_os = "android")] pub mod server_side { - use hbb_common::log; + use hbb_common::{log, config}; use jni::{ objects::{JClass, JString}, sys::jstring, @@ -1374,11 +1374,25 @@ pub mod server_side { pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer( env: JNIEnv, _class: JClass, + app_dir: JString, ) { - log::debug!("startServer from java"); + log::debug!("startServer from jvm"); + if let Ok(app_dir) = env.get_string(app_dir) { + *config::APP_DIR.write().unwrap() = app_dir.into(); + } std::thread::spawn(move || start_server(true)); } + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startService( + env: JNIEnv, + _class: JClass, + ) { + log::debug!("startService from jvm"); + config::Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + #[no_mangle] pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_translateLocale( env: JNIEnv, diff --git a/src/lang/ca.rs b/src/lang/ca.rs index aa33ae6e5..53ec69b5f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), ("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Connexió no disponible"), ("Legacy mode", "Mode heretat"), ("Map mode", "Mode mapa"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index f975e343f..4c037234b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "保持 RustDesk 后台服务"), ("Ignore Battery Optimizations", "忽略电池优化"), ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), + ("Start on Boot", "开机自启动"), + ("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), ("Map mode", "1:1 传输"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index cfe69924c..25a494eef 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), @@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), - ("No transfers in progress", ""), ("Codec", ""), ("Resolution", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 19310357b..8fd6f9be1 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), ("Ignore Battery Optimizations", "Ignorer betteri optimeringer"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Forbindelse ikke tilladt"), ("Legacy mode", "Bagudkompatibilitetstilstand"), ("Map mode", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index ee28fe0e7..754d7b9ef 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), ("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"), ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), ("Map mode", "Kartenmodus"), @@ -454,8 +456,8 @@ 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", ""), - ("No transfers in progress", ""), + ("Codec", "Codec"), + ("Resolution", "Auflösung"), + ("No transfers in progress", "Keine Übertragungen im Gange"), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 3e87cd661..250530013 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -39,8 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."), ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), - ("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."), + ("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."), ("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."), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 9b7912cff..dfee4fb87 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index af0da0479..dc28cdae0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), @@ -456,6 +458,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reconnect", "Reconectar"), ("Codec", "Códec"), ("Resolution", "Resolución"), - ("No transfers in progress", ""), + ("No transfers in progress", "No hay transferencias en curso"), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 0c31e1531..824bd039c 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"), ("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"), ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "اتصال مجاز نیست"), ("Legacy mode", "legacy حالت"), ("Map mode", "map حالت"), @@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), ("Reconnect", "اتصال مجدد"), - ("No transfers in progress", ""), - ("Codec", ""), - ("Resolution", ""), + ("Codec", "کدک"), + ("Resolution", "وضوح"), + ("No transfers in progress", "هیچ انتقالی در حال انجام نیست"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 0e45827f7..28f1dd9d1 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Connexion non autorisée"), ("Legacy mode", "Mode hérité"), ("Map mode", ""), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index fca98f228..55a3c9bb7 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"), ("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Η σύνδεση απορρίφθηκε"), ("Legacy mode", "Λειτουργία συμβατότητας"), ("Map mode", "Map mode"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 437cf445a..f47d522db 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk futtatása a háttérben"), ("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívűl hagyása"), ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállítási oldalára, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "A csatlakozás nem engedélyezett"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 84892a7f8..7d02e154d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Pertahankan RustDesk berjalan pada background service"), ("Ignore Battery Optimizations", "Abaikan Pengoptimalan Baterai"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Koneksi tidak dijinkan"), ("Legacy mode", "Mode lama"), ("Map mode", "Mode peta"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 101685c4a..8aedc04f6 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"), ("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"), ("android_open_battery_optimizations_tip", "Se si desidera disabilitare questa funzione, andare nelle impostazioni dell'applicazione RustDesk, aprire la sezione [Batteria] e deselezionare [Senza restrizioni]."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Connessione non consentita"), ("Legacy mode", "Modalità legacy"), ("Map mode", "Modalità mappa"), @@ -454,8 +456,8 @@ 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"), - ("No transfers in progress", "Nessun trasferimento in corso"), ("Codec", "Codec"), ("Resolution", "Risoluzione"), + ("No transfers in progress", "Nessun trasferimento in corso"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c19b607ca..d097a8b61 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk バックグラウンドサービスを維持"), ("Ignore Battery Optimizations", "バッテリーの最適化を無効にする"), ("android_open_battery_optimizations_tip", "この機能を使わない場合は、次のRestDeskアプリ設定ページから「バッテリー」に進み、「制限なし」の選択を外してください"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "接続が許可されていません"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 97574e67d..8ca881f16 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk 백그라운드 서비스로 유지하기"), ("Ignore Battery Optimizations", "배터리 최적화 무시하기"), ("android_open_battery_optimizations_tip", "해당 기능을 비활성화하려면 RustDesk 응용 프로그램 설정 페이지로 이동하여 [배터리]에서 [제한 없음] 선택을 해제하십시오."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "연결이 허용되지 않음"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 54a51b439..a9acdce65 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Қосылу рұқсат етілмеген"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index eb7c214ab..d1c154546 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Verbinding niet toegestaan"), ("Legacy mode", "Verouderde modus"), ("Map mode", "Map mode"), @@ -444,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default View Style", "Standaard Weergave Stijl"), ("Default Scroll Style", "Standaard Scroll Stijl"), ("Default Image Quality", "Standaard Beeldkwaliteit"), - ("Default Codec", "tandaard Codec"), + ("Default Codec", "Standaard Codec"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), ("Auto", "Auto"), @@ -452,10 +454,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Spraakoproep"), ("Text chat", "Tekst chat"), ("Stop voice call", "Stop spraakoproep"), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), + ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server. \nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), + ("Reconnect", "Herverbinden"), + ("Codec", "Codec"), + ("Resolution", "Resolutie"), + ("No transfers in progress", "Geen overdrachten in uitvoering"), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 13027a682..494715527 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"), ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"), ("android_open_battery_optimizations_tip", "Jeśli chcesz wyłączyć tę funkcję, przejdź do następnej strony ustawień aplikacji RustDesk, znajdź i wprowadź [Bateria], odznacz [Bez ograniczeń]"), + ("Start on Boot", "Autostart"), + ("Start the screen sharing service on boot, requires special permissions", "Uruchom usługę udostępniania ekranu podczas startu, wymaga specjalnych uprawnień"), ("Connection not allowed", "Połączenie niedozwolone"), ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), ("Map mode", "Tryb mapowania"), @@ -456,9 +458,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reconnect", "Połącz ponownie"), ("Codec", "Kodek"), ("Resolution", "Rozdzielczość"), - ("Use temporary password", "Użyj hasła tymczasowego"), - ("Set temporary password length", "Ustaw długość hasła tymczasowego"), ("Key", "Klucz"), - ("No transfers in progress", ""), + ("No transfers in progress", "Brak transferów w toku"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 923bbab05..b62bd5a31 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Manter o serviço RustDesk em funcionamento"), ("Ignore Battery Optimizations", "Ignorar optimizações de Bateria"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Ligação não autorizada"), ("Legacy mode", ""), ("Map mode", ""), @@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), - ("No transfers in progress", ""), ("Codec", ""), ("Resolution", ""), + ("No transfers in progress", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index aa491f951..546ef2a3c 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Manter o serviço do RustDesk executando em segundo plano"), ("Ignore Battery Optimizations", "Ignorar otimizações de bateria"), ("android_open_battery_optimizations_tip", "Abrir otimizações de bateria"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexão não permitida"), ("Legacy mode", "Modo legado"), ("Map mode", "Modo mapa"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index e992b19d8..af9389a29 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Rulează serviciul RustDesk în fundal"), ("Ignore Battery Optimizations", "Ignoră optimizările de baterie"), ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexiune neautoriztă"), ("Legacy mode", "Mod legacy"), ("Map mode", "Mod hartă"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 07b8af998..b9af4ce98 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Держать в фоне службу RustDesk"), ("Ignore Battery Optimizations", "Игнорировать оптимизацию батареи"), ("android_open_battery_optimizations_tip", "Перейдите на следующую страницу настроек"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Подключение не разрешено"), ("Legacy mode", "Устаревший режим"), ("Map mode", "Режим сопоставления"), @@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), ("Reconnect", "Переподключить"), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), + ("Codec", "Кодек"), + ("Resolution", "Разрешение"), + ("No transfers in progress", "Передача не осуществляется"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6468b7eef..8a6b765be 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index d128e7322..5721d01f4 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Povezava ni dovoljena"), ("Legacy mode", "Stari način"), ("Map mode", "Način preslikave"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 29c5cbbf8..1c488d470 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mbaje shërbimin e sfondit të RustDesk"), ("Ignore Battery Optimizations", "Injoro optimizimet e baterisë"), ("android_open_battery_optimizations_tip", "Nëse dëshironi ta çaktivizoni këtë veçori, ju lutemi shkoni te faqja tjetër e cilësimeve të aplikacionit RustDesk, gjeni dhe shtypni [Batteri], hiqni zgjedhjen [Te pakufizuara]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Lidhja nuk lejohet"), ("Legacy mode", "Modaliteti i trashëgimisë"), ("Map mode", "Modaliteti i hartës"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 63173dc11..249c0b599 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Zadrži RustDesk kao pozadinski servis"), ("Ignore Battery Optimizations", "Zanemari optimizacije baterije"), ("android_open_battery_optimizations_tip", "Ako želite da onemogućite ovu funkciju, molimo idite na sledeću stranicu za podešavanje RustDesk aplikacije, pronađite i uđite u [Battery], isključite [Unrestricted]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Konekcija nije dozvoljena"), ("Legacy mode", "Zastareli mod"), ("Map mode", "Mod mapiranja"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 1a00ece43..90ec8c1cf 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Behåll RustDesk i bakgrunden"), ("Ignore Battery Optimizations", "Ignorera batterioptimering"), ("android_open_battery_optimizations_tip", "Om du vill stänga av denna funktion, gå till nästa RustDesk programs inställningar, hitta [Batteri], Checka ur [Obegränsad]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Anslutning ej tillåten"), ("Legacy mode", "Legacy mode"), ("Map mode", "Kartläge"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 2c83f9474..6563d6056 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 6fcf02ed2..316622395 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index d35d288d6..7359bf064 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), ("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "bağlantıya izin verilmedi"), ("Legacy mode", "Eski mod"), ("Map mode", "Haritalama modu"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 20a2998ec..70533c482 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "保持RustDesk後台服務"), ("Ignore Battery Optimizations", "忽略電池優化"), ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的RustDesk應用設置頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "對方不允許連接"), ("Legacy mode", "傳統模式"), ("Map mode", "1:1傳輸"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 4c4b5d4bc..6b54c83c3 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Зберегти фонову службу RustDesk"), ("Ignore Battery Optimizations", "Ігнорувати оптимізацію батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Підключення не дозволено"), ("Legacy mode", "Застарілий режим"), ("Map mode", "Режим карти"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 32cd084cb..a379b3185 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Giữ dịch vụ nền RustDesk"), ("Ignore Battery Optimizations", "Bỏ qua các tối ưu pin"), ("android_open_battery_optimizations_tip", "Nếu bạn muốn tắt tính năng này, vui lòng chuyển đến trang cài đặt ứng dụng RustDesk tiếp theo, tìm và nhập [Pin], Bỏ chọn [Không hạn chế]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Kết nối không đuợc phép"), ("Legacy mode", ""), ("Map mode", ""), diff --git a/src/main.rs b/src/main.rs index 169515425..3759f6056 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -// Specify the Windows subsystem to eliminate console window. -// Requires Rust 1.18. -//#![windows_subsystem = "windows"] + #![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" + )] use librustdesk::*; diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 8be0c6db5..3c90981c4 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -1,6 +1,9 @@ #import #import #import +#include +#include + // https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm @@ -35,6 +38,33 @@ extern "C" bool InputMonitoringAuthStatus(bool prompt) { return false; } +extern "C" bool MacCheckAdminAuthorization() { + AuthorizationRef authRef; + OSStatus status; + + status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults, &authRef); + if (status != errAuthorizationSuccess) { + printf("Failed to create AuthorizationRef\n"); + return false; + } + + AuthorizationItem authItem = {kAuthorizationRightExecute, 0, NULL, 0}; + AuthorizationRights authRights = {1, &authItem}; + AuthorizationFlags flags = kAuthorizationFlagDefaults | + kAuthorizationFlagInteractionAllowed | + kAuthorizationFlagPreAuthorize | + kAuthorizationFlagExtendRights; + status = AuthorizationCopyRights(authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL); + if (status != errAuthorizationSuccess) { + printf("Failed to authorize\n"); + return false; + } + + AuthorizationFree(authRef, kAuthorizationFlagDefaults); + return true; +} + extern "C" float BackingScaleFactor() { NSScreen* s = [NSScreen mainScreen]; if (s) return [s backingScaleFactor]; @@ -44,6 +74,33 @@ extern "C" float BackingScaleFactor() { // https://github.com/jhford/screenresolution/blob/master/cg_utils.c // https://github.com/jdoupe/screenres/blob/master/setgetscreen.m +size_t bitDepth(CGDisplayModeRef mode) { + size_t depth = 0; + // Deprecated, same display same bpp? + // https://stackoverflow.com/questions/8210824/how-to-avoid-cgdisplaymodecopypixelencoding-to-get-bpp + // https://github.com/libsdl-org/SDL/pull/6628 + 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; +} + extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); if (allModes == NULL) { @@ -55,16 +112,28 @@ extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { } 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) { + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { return false; } - *numModes = CFArrayGetCount(allModes); - for (uint32_t 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); + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return false; } + uint32_t allModeCount = CFArrayGetCount(allModes); + uint32_t realNum = 0; + for (uint32_t i = 0; i < allModeCount && realNum < max; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && + bitDepth(currentMode) == bitDepth(mode)) { + widths[realNum] = (uint32_t)CGDisplayModeGetWidth(mode); + heights[realNum] = (uint32_t)CGDisplayModeGetHeight(mode); + realNum++; + } + } + *numModes = realNum; + CGDisplayModeRelease(currentMode); CFRelease(allModes); return true; } @@ -80,31 +149,8 @@ extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t 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) { +static bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { CGError rc; CGDisplayConfigRef config; rc = CGBeginDisplayConfiguration(&config); @@ -122,7 +168,6 @@ bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { return true; } - extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height) { bool ret = false; @@ -140,8 +185,8 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); if (width == CGDisplayModeGetWidth(mode) && height == CGDisplayModeGetHeight(mode) && - bitDepth(currentMode) == bitDepth(mode) && - CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode)) { + CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && + bitDepth(currentMode) == bitDepth(mode)) { ret = setDisplayToMode(display, mode); break; } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b663b0f41..5c4c68e2c 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -34,6 +34,7 @@ extern "C" { static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; + fn MacCheckAdminAuthorization() -> BOOL; fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL; fn MacGetModes( display: u32, @@ -665,3 +666,10 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType< } Ok(()) } + + +pub fn check_super_user_permission() -> ResultType { + unsafe { + Ok(MacCheckAdminAuthorization() == YES) + } +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6b3f8013c..561bb4570 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -976,7 +976,7 @@ fn get_after_install(exe: &str) -> String { } pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> { - let uninstall_str = get_uninstall(); + let uninstall_str = get_uninstall(false); let mut path = path.trim_end_matches('\\').to_owned(); let (subkey, _path, start_menu, exe) = get_default_install_info(); let mut exe = exe; @@ -1188,30 +1188,35 @@ pub fn run_after_install() -> ResultType<()> { } pub fn run_before_uninstall() -> ResultType<()> { - run_cmds(get_before_uninstall(), true, "before_install") + run_cmds(get_before_uninstall(true), true, "before_install") } -fn get_before_uninstall() -> String { +fn get_before_uninstall(kill_self: bool) -> String { let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); + let filter = if kill_self { + "".to_string() + } else { + format!(" /FI \"PID ne {}\"", get_current_pid()) + }; format!( " chcp 65001 sc stop {app_name} sc delete {app_name} taskkill /F /IM {broker_exe} - taskkill /F /IM {app_name}.exe /FI \"PID ne {cur_pid}\" + taskkill /F /IM {app_name}.exe{filter} reg delete HKEY_CLASSES_ROOT\\.{ext} /f netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", app_name = app_name, broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ext = ext, - cur_pid = get_current_pid(), + filter = filter, ) } -fn get_uninstall() -> String { +fn get_uninstall(kill_self: bool) -> String { let (subkey, path, start_menu, _) = get_install_info(); format!( " @@ -1222,7 +1227,7 @@ fn get_uninstall() -> String { if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\" if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" ", - before_uninstall=get_before_uninstall(), + before_uninstall=get_before_uninstall(kill_self), subkey=subkey, app_name = crate::get_app_name(), path = path, @@ -1231,11 +1236,20 @@ fn get_uninstall() -> String { } pub fn uninstall_me() -> ResultType<()> { - run_cmds(get_uninstall(), true, "uninstall") + run_cmds(get_uninstall(true), true, "uninstall") } fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType { let mut tmp = std::env::temp_dir(); + // When dir contains these characters, the bat file will not execute in elevated mode. + if vec!["&", "@", "^"] + .drain(..) + .any(|s| tmp.to_string_lossy().to_string().contains(s)) + { + if let Ok(dir) = user_accessible_folder() { + tmp = dir; + } + } tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext)); let mut file = std::fs::File::create(&tmp)?; // in case cmds mixed with \r\n and \n, make sure all ending with \r\n @@ -1872,3 +1886,19 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType< Ok(()) } } + +pub fn user_accessible_folder() -> ResultType { + let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); + let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); + // NOTICE: "C:\Windows\Temp" requires permanent authorization. + let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); + let dir; + if dir1.exists() { + dir = dir1; + } else if dir2.exists() { + dir = dir2; + } else { + bail!("no vaild user accessible folder"); + } + Ok(dir) +} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 7514ead38..c49f974a7 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -117,17 +117,7 @@ impl SharedMemory { } fn flink(name: String) -> ResultType { - let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); - let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); - let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); - let mut dir; - if dir1.exists() { - dir = dir1; - } else if dir2.exists() { - dir = dir2; - } else { - bail!("no vaild flink directory"); - } + let mut dir = crate::platform::user_accessible_folder()?; dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); if !dir.exists() { std::fs::create_dir(&dir)?; diff --git a/src/ui/header.tis b/src/ui/header.tis index e25c0d544..257ba417e 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -198,6 +198,7 @@ class Header: Reactor.Component { {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ?
  • {svg_checkmark}{translate('Swap control-command key')}
  • : ""} ; } @@ -440,7 +441,7 @@ function toggleMenuState() { for (var el in $$(menu#keyboard-options>li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } - for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) { + for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key"]) { var el = self.select('#' + id); if (el) { var value = handler.get_toggle_option(id); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 62eba25c1..471150f60 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -707,10 +707,10 @@ pub fn is_root() -> bool { pub fn check_super_user_permission() -> bool { #[cfg(feature = "flatpak")] return true; - #[cfg(any(windows, target_os = "linux"))] + #[cfg(any(windows, target_os = "linux", target_os = "macos"))] return crate::platform::check_super_user_permission().unwrap_or(false); - #[cfg(not(any(windows, target_os = "linux")))] - true + #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] + return true; } #[allow(dead_code)] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f726ed526..11bcff925 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -373,10 +373,87 @@ impl Session { return "".to_owned(); } + pub fn swab_modifier_key(&self, msg: &mut KeyEvent) { + + let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string()); + if allow_swap_key { + if let Some(key_event::Union::ControlKey(ck)) = msg.union { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + msg.set_control_key(ck); + } + msg.modifiers = msg.modifiers.iter().map(|ck| { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + hbb_common::protobuf::EnumOrUnknown::new(ck) + }).collect(); + + + let code = msg.chr(); + if code != 0 { + let mut peer = self.peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + + let key = match peer.as_str() { + "windows" => { + let key = rdev::win_key_from_scancode(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::win_scancode_from_key(key).unwrap_or_default() + } + "macos" => { + let key = rdev::macos_key_from_code(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::macos_keycode_from_key(key).unwrap_or_default() + } + _ => { + let key = rdev::linux_key_from_code(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::linux_keycode_from_key(key).unwrap_or_default() + } + }; + msg.set_chr(key); + } + } + + } + pub fn send_key_event(&self, evt: &KeyEvent) { // mode: legacy(0), map(1), translate(2), auto(3) + + let mut msg = evt.clone(); + self.swab_modifier_key(&mut msg); let mut msg_out = Message::new(); - msg_out.set_key_event(evt.clone()); + msg_out.set_key_event(msg); self.send(Data::Message(msg_out)); } @@ -934,6 +1011,23 @@ impl Interface for Session { handle_test_delay(t, peer).await; } } + + fn swap_modifier_mouse(&self, msg : &mut hbb_common::protos::message::MouseEvent) { + let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string()); + if allow_swap_key { + msg.modifiers = msg.modifiers.iter().map(|ck| { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + hbb_common::protobuf::EnumOrUnknown::new(ck) + }).collect(); + }; + } } impl Session { diff --git a/vdi/host/.devcontainer/devcontainer.json b/vdi/host/.devcontainer/devcontainer.json index 02c6e589b..f0016b5b1 100644 --- a/vdi/host/.devcontainer/devcontainer.json +++ b/vdi/host/.devcontainer/devcontainer.json @@ -4,8 +4,8 @@ "dockerfile": "./Dockerfile", "context": "." }, - "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", - "workspaceFolder": "/home/vscode/rustdesk", + "workspaceMount": "source=${localWorkspaceFolder}/../..,target=/home/vscode/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/vscode/rustdesk/vdi/host", "customizations": { "vscode": { "extensions": [ @@ -15,6 +15,7 @@ "tamasfe.even-better-toml", "serayuzgur.crates", "mhutchie.git-graph", + "formulahendry.terminal", "eamodio.gitlens" ], "settings": { diff --git a/vdi/host/Cargo.lock b/vdi/host/Cargo.lock index d4254717e..7b7cf26bd 100644 --- a/vdi/host/Cargo.lock +++ b/vdi/host/Cargo.lock @@ -2,6 +2,32 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -11,6 +37,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + [[package]] name = "async-broadcast" version = "0.3.4" @@ -73,7 +114,7 @@ dependencies = [ "parking", "polling", "slab", - "socket2", + "socket2 0.4.7", "waker-fn", "windows-sys 0.42.0", ] @@ -116,29 +157,73 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -146,6 +231,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.1.0" @@ -155,6 +265,57 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "confy" +version = "0.4.0" +source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" +dependencies = [ + "directories-next", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.7.1", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.14" @@ -164,6 +325,50 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cxx" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derivative" version = "2.2.0" @@ -175,6 +380,16 @@ dependencies = [ "syn", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "4.0.0" @@ -184,6 +399,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -195,12 +420,38 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "easy-parallel" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "enumflags2" version = "0.7.5" @@ -222,6 +473,19 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -237,6 +501,18 @@ dependencies = [ "instant", ] +[[package]] +name = "filetime" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.45.0", +] + [[package]] name = "futures" version = "0.3.26" @@ -349,14 +625,78 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hbb_common" +version = "0.1.0" +dependencies = [ + "anyhow", + "backtrace", + "bytes", + "chrono", + "confy", + "directories-next", + "dirs-next", + "env_logger", + "filetime", + "futures", + "futures-util", + "lazy_static", + "libc", + "log", + "mac_address", + "machine-uid", + "osascript", + "protobuf", + "protobuf-codegen", + "rand", + "regex", + "serde", + "serde_derive", + "socket2 0.3.19", + "sodiumoxide", + "sysinfo", + "tokio", + "tokio-socks", + "tokio-util", + "winapi", + "zstd", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] [[package]] name = "hex" @@ -364,6 +704,36 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -383,12 +753,54 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + [[package]] name = "libusb1-sys" version = "0.6.4" @@ -401,6 +813,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -420,6 +841,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mac_address" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" +dependencies = [ + "nix 0.23.2", + "winapi", +] + +[[package]] +name = "machine-uid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212" +dependencies = [ + "winreg", +] + [[package]] name = "memchr" version = "2.5.0" @@ -435,6 +875,49 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nix" version = "0.24.3" @@ -444,7 +927,7 @@ dependencies = [ "bitflags", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -456,6 +939,53 @@ dependencies = [ "memchr", ] +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "object" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -472,6 +1002,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "osascript" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "parking" version = "2.0.0" @@ -501,6 +1042,26 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -558,6 +1119,58 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "protobuf" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +dependencies = [ + "thiserror", +] + [[package]] name = "qemu-display" version = "0.1.0" @@ -588,6 +1201,7 @@ dependencies = [ name = "qemu-rustdesk" version = "0.1.0" dependencies = [ + "hbb_common", "qemu-display", ] @@ -630,6 +1244,28 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -686,12 +1322,39 @@ dependencies = [ "libusb1-sys", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + [[package]] name = "serde" version = "1.0.152" @@ -733,6 +1396,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.10" @@ -759,6 +1433,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + [[package]] name = "slab" version = "0.4.8" @@ -774,6 +1463,17 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.4.7" @@ -784,6 +1484,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519", + "libc", + "libsodium-sys", + "serde", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -801,6 +1513,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -815,6 +1542,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.38" @@ -835,6 +1571,91 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tokio" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.4.7", + "tokio-macros", + "windows-sys 0.42.0", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1-1" +source = "git+https://github.com/open-trade/tokio-socks#7034e79263ce25c348be072808d7601d82cd892d" +dependencies = [ + "bytes", + "either", + "futures-core", + "futures-sink", + "futures-util", + "pin-project", + "thiserror", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "hashbrown", + "pin-project-lite", + "slab", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.5.1" @@ -900,6 +1721,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "usbredirhost" version = "0.0.1" @@ -948,18 +1775,95 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "waker-fn" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + [[package]] name = "wepoll-ffi" version = "0.1.2" @@ -969,6 +1873,17 @@ dependencies = [ "cc", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" @@ -985,6 +1900,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1087,6 +2011,15 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi", +] + [[package]] name = "xml-rs" version = "0.8.4" @@ -1116,7 +2049,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.24.3", "once_cell", "ordered-stream", "rand", @@ -1157,6 +2090,35 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zstd" +version = "0.9.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.3+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "zvariant" version = "3.11.0" diff --git a/vdi/host/Cargo.toml b/vdi/host/Cargo.toml index 62c964122..6a67813a2 100644 --- a/vdi/host/Cargo.toml +++ b/vdi/host/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" authors = ["rustdesk "] edition = "2021" - [dependencies] -qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" } \ No newline at end of file +qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" } +hbb_common = { path = "../../libs/hbb_common" } From 05c0edca49a898698fdff87e033d9bb5d783517f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 2 Mar 2023 09:34:08 +0800 Subject: [PATCH 729/734] fix: add texture rgba renderer so to rpm build --- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 6c7055b4a..77c28a94e 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -4,7 +4,7 @@ Release: 0 Summary: RPM package License: GPL-3.0 Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libappindicator-gtk3 libvdpau1 libva2 -Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit) +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description The best open-source remote desktop client software, written in Rust. diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 73bb993aa..6124cbb70 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -4,7 +4,7 @@ Release: 0 Summary: RPM package License: GPL-3.0 Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator-gtk3 libvdpau libva -Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit) +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description The best open-source remote desktop client software, written in Rust. From c5d7e5dfdad00da79985eec843acf75e56b776d5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 2 Mar 2023 13:32:21 +0800 Subject: [PATCH 730/734] fix: #3369 --- src/{ui/macos.rs => platform/delegate.rs} | 11 +++++++---- src/platform/mod.rs | 3 +++ src/tray.rs | 4 ++++ src/ui.rs | 6 ++---- 4 files changed, 16 insertions(+), 8 deletions(-) rename src/{ui/macos.rs => platform/delegate.rs} (97%) diff --git a/src/ui/macos.rs b/src/platform/delegate.rs similarity index 97% rename from src/ui/macos.rs rename to src/platform/delegate.rs index cd0e5871b..01855536e 100644 --- a/src/ui/macos.rs +++ b/src/platform/delegate.rs @@ -26,7 +26,7 @@ const SHOW_SETTINGS_TAG: u32 = 2; const RUN_ME_TAG: u32 = 3; const AWAKE: u32 = 4; -trait AppHandler { +pub trait AppHandler { fn command(&mut self, cmd: u32); } @@ -63,9 +63,12 @@ impl AppHandler for Rc { } // https://github.com/xi-editor/druid/blob/master/druid-shell/src/platform/mac/application.rs -unsafe fn set_delegate(handler: Option>) { - let mut decl = - ClassDecl::new("AppDelegate", class!(NSObject)).expect("App Delegate definition failed"); +pub unsafe fn set_delegate(handler: Option>) { + let decl = ClassDecl::new("AppDelegate", class!(NSObject)); + if decl.is_none() { + return; + } + let mut decl = decl.unwrap(); decl.add_ivar::<*mut c_void>(APP_HANDLER_IVAR); decl.add_method( diff --git a/src/platform/mod.rs b/src/platform/mod.rs index ad058d4c0..f2b609d3f 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -11,6 +11,9 @@ pub mod windows; #[cfg(target_os = "macos")] pub mod macos; +#[cfg(target_os = "macos")] +pub mod delegate; + #[cfg(target_os = "linux")] pub mod linux; diff --git a/src/tray.rs b/src/tray.rs index 617ec2c93..38ed9b0cb 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -131,6 +131,10 @@ pub fn make_tray() -> hbb_common::ResultType<()> { let event_loop = EventLoopBuilder::new().build(); + unsafe { + crate::platform::delegate::set_delegate(None); + } + let tray_menu = Menu::new(); let quit_i = MenuItem::new(crate::client::translate("Exit".to_owned()), true, None); tray_menu.append_items(&[&quit_i]); diff --git a/src/ui.rs b/src/ui.rs index 22c44ec5f..f7419cd34 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -20,8 +20,6 @@ use crate::{common::get_app_name, ipc, ui_interface::*}; mod cm; #[cfg(feature = "inline")] pub mod inline; -#[cfg(target_os = "macos")] -pub mod macos; pub mod remote; pub type Children = Arc)>>; @@ -43,7 +41,7 @@ struct UIHostHandler; pub fn start(args: &mut [String]) { #[cfg(target_os = "macos")] - macos::show_dock(); + crate::platform::delegate::show_dock(); #[cfg(all(target_os = "linux", feature = "inline"))] { #[cfg(feature = "appimage")] @@ -75,7 +73,7 @@ pub fn start(args: &mut [String]) { allow_err!(sciter::set_options(sciter::RuntimeOptions::UxTheming(true))); frame.set_title(&crate::get_app_name()); #[cfg(target_os = "macos")] - macos::make_menubar(frame.get_host(), args.is_empty()); + crate::platform::delegate::make_menubar(frame.get_host(), args.is_empty()); let page; if args.len() > 1 && args[0] == "--play" { args[0] = "--connect".to_owned(); From 381b1c0cc69b6bd34a7d5023dfa8cdb3bcb87ec4 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Thu, 2 Mar 2023 09:16:25 +0330 Subject: [PATCH 731/734] Update fa.rs --- src/lang/fa.rs | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 824bd039c..158e16f75 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -37,23 +37,23 @@ 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", ""), + ("Your new ID", "جدید ID"), + ("length %min% to %max%", "%max% تا %min% طول از"), + ("starts with a letter", "با حرف شروع می شود"), + ("allowed characters", "کارکترهای مجاز"), ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), ("Website", "وب سایت"), ("About", "درباره"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "ساخته شده با قلب(عشق) در این دنیای پر هرج و مرج!"), + ("Privacy Statement", "بیانیه حریم خصوصی"), ("Mute", "بستن صدا"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "تاریخ ساخت"), + ("Version", "نسخه"), + ("Home", "صفحه اصلی"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), ("Hardware Codec", "کدک سخت افزاری"), - ("Adaptive Bitrate", ""), + ("Adaptive Bitrate", "سازگار Bitrate"), ("ID Server", "شناسه سرور"), ("Relay Server", "Relay سرور"), ("API Server", "API سرور"), @@ -213,7 +213,7 @@ 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", ""), + ("Connect via relay", "اتصال با رله"), ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), @@ -312,8 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"), ("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"), ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), - ("Start on Boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on Boot", "در هنگام بوت شروع شود"), + ("Start the screen sharing service on boot, requires special permissions", "سرویس اشتراک‌گذاری صفحه را در بوت راه‌اندازی کنید، به مجوزهای خاصی نیاز دارد"), ("Connection not allowed", "اتصال مجاز نیست"), ("Legacy mode", "legacy حالت"), ("Map mode", "map حالت"), @@ -381,8 +381,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "غیر فعالسازی جستجو در شبکه"), ("Write a message", "یک پیام بنویسید"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "باشید UAC لطفا منتظر تایید"), + ("elevated_foreground_window_tip", "پنجره فعلی دسکتاپ راه دور برای کار کردن به دسترسی بالاتری نیاز دارد، بنابراین نمی‌تواند به طور موقت از ماوس و صفحه کلید استفاده کند. می توانید از کاربر راه دور درخواست کنید که پنجره فعلی را به پایین منتقل کند یا روی دکمه ارتقاء دسترسی در پنجره مدیریت اتصال کلیک کنید. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی دستگاه از راه دور نصب کنید."), ("Disconnected", "قطع ارتباط"), ("Other", "سایر"), ("Confirm before closing multiple tabs", "تایید بستن دسته ای برگه ها"), @@ -397,7 +397,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "This PC"), ("or", "یا"), ("Continue with", "ادامه با"), - ("Elevate", "افزایش سطح"), + ("Elevate", "ارتقاء"), ("Zoom cursor", " بزرگنمایی نشانگر ماوس"), ("Accept sessions via password", "قبول درخواست با رمز عبور"), ("Accept sessions via click", "قبول درخواست با کلیک موس"), @@ -421,17 +421,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), - ("config_microphone", ""), - ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), + ("config_microphone", "را بدهید. RustDesk \"Record Audio\" برای صحبت از راه دور، باید مجوز"), + ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتقاء دسترسی دهید."), ("Wait", "صبر کنید"), - ("Elevation Error", "خطای ارتفاع"), + ("Elevation Error", "خطای ارتقاء دسترسی"), ("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", "همچنان کاربر از راه دور نیاز دارد که روی OK در پنجره UAC اجرای RustDesk کلیک کند."), - ("Request Elevation", "درخواست ارتفاع"), + ("Request Elevation", "درخواست ارتقاء دسترسی"), ("wait_accept_uac_tip", "لطفاً منتظر بمانید تا کاربر راه دور درخواست پنجره UAC را بپذیرد."), - ("Elevate successfully", "با موفقیت بالا ببرید"), + ("Elevate successfully", "ارتقاء دسترسی با موفقیت انجام شد"), ("uppercase", "حروف بزرگ"), ("lowercase", "حروف کوچک"), ("digit", "عدد"), From 9388837c4217b03077131b89765f47506c415aee Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:18:12 +0800 Subject: [PATCH 732/734] Update pubspec.lock --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 2202b2ccf..76fe929e5 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 336e826ab9bfb5b09cee65b6874842351c5fa779 Mon Sep 17 00:00:00 2001 From: Georg Stadler Date: Thu, 2 Mar 2023 10:20:12 +0100 Subject: [PATCH 733/734] Update de.rs Added two lines of German translation --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 754d7b9ef..d6ebe7f1a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -312,8 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), ("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"), ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"), - ("Start on Boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on Boot", "Beim Booten Starten"), + ("Start the screen sharing service on boot, requires special permissions", "Bildschirmfreigabedienst beim Booten starten, benötigt zusätzliche Berechtigungen"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), ("Map mode", "Kartenmodus"), From e198f3ac21e4cc6e00c674154a9b8d10531570b0 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Thu, 2 Mar 2023 13:49:23 +0100 Subject: [PATCH 734/734] Update it.rs --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 8aedc04f6..05fffedbd 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -312,8 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"), ("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"), ("android_open_battery_optimizations_tip", "Se si desidera disabilitare questa funzione, andare nelle impostazioni dell'applicazione RustDesk, aprire la sezione [Batteria] e deselezionare [Senza restrizioni]."), - ("Start on Boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on Boot", "Avvia all'accensione"), + ("Start the screen sharing service on boot, requires special permissions", "L'avvio del servizio di condivisione dello schermo all'accensione, richiede autorizzazioni speciali"), ("Connection not allowed", "Connessione non consentita"), ("Legacy mode", "Modalità legacy"), ("Map mode", "Modalità mappa"),

    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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 442/734] 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 443/734] 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 444/734] 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 445/734] 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 446/734] 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 447/734] 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 448/734] 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 449/734] 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 450/734] 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 451/734] 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 452/734] 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 453/734] 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 454/734] 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 455/734] 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 456/734] 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 457/734] 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