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 021d8573..7a1ae659 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 00000000..10979f52 --- /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 03bbf591..1bb2ff86 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 9023780f..0f379426 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 ae564685..4245b1d7 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 0f379426..02b4a0c1 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 0cf2f2e2..e7ac620e 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 c1a7bdce..fc35fb47 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 ddfaad06..61b01f50 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 9fa53757..f29eb27b 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 716f2c6d..a1d62332 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 63df0cb0..5c828689 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 635c8b66..440c0b0b 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 41ce8fd9..814fea11 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 39301fd0..3a7920bc 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 1f35ef92..f78b3a20 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 0137b784..63f86078 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 af3142ad..5ad7bc24 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 c77da3f8..d92ed2a7 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 b986c785..618b003e 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 24b3be11..68b9c37c 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 cfbdb0c4..58a8f710 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 b992e39e..e7a533d6 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 e6f40e21..f0fe41b8 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 c562225b..9048db76 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 f91281a5..087dbde4 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 695d6041..a32662d0 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 70e38eb5..ae996b68 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 650e4210..de0d6e7c 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 2846d907..ce6d2668 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 6d2e92ae..5d96a330 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 3b7d1c2c..c628f018 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 d92ed2a7..9cd6077a 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 47e6d53c..e2e4bd4a 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 d38fbf22..e4736c0e 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 4c6375dd..a6ae2a9e 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 a1d62332..4e46e217 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 37bbfb3b..481d6555 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 767d6479..909f1742 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 fcc2981f..2794552b 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 087dbde4..9d7470f4 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 f3165a68..31fd632e 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 ce27ceb2..15105ae6 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 9d7470f4..0683b486 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 e2e4bd4a..d9be7b43 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 1f81dfa5..5ec7890b 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 c03d4c57..a9fd8408 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 5cf60336..926941b8 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 77e52ff0..17679112 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 2794552b..f55f3dac 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 79255e45..bc9bacf1 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 ac92da14..9f2dc988 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 b2458d09..f513a1c6 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 440c0b0b..cd05586f 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 ab3fb907..835fd87b 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 5d96a330..4e3d3d1d 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 85749a25..2dae0325 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 d5bd0164..52af0069 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 930d2071..b5655b2f 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 00ef7254..a66150c4 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 f55f3dac..cd220843 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 de0d6e7c..e39bc7c6 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 9535e9f3..acfd4c67 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 ae996b68..204993c1 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 5ec7890b..71d353c8 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 cd220843..509bbf97 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 2ff6d686..c15819df 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 ce6d2668..b0c479c9 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 73c017e2..8b7dae1b 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 c628f018..9984198b 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 17679112..94a7bcd5 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 5829686b..152f502a 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 0683b486..cbd13023 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 4e3d3d1d..6b87da21 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 b718fc0f..14d221ef 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 3eb8f0b8..0e4f98d5 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 2d0b1bf7..0a6cd8ef 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 91ce6cce..d428bcb9 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 cbd13023..c29faa72 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 f29eb27b..a11b0e97 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 6fd205a2..6593f180 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 eb13edf1..aaafede0 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 00000000..12d73771 --- /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 aaafede0..e1dfdbe7 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 12d73771..ee37baf1 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 ee37baf1..f3cd8f56 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 ae95fd2c..59dd5164 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 65970270..86707fd6 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 7825286b..df47d07d 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 08ad36eb..76ced87a 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 00000000..9497cc44 --- /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 5c020261..381e3df9 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 c29faa72..9764820f 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 df47d07d..738b57c4 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 017850cf..2d0dcacd 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 e8361a65..bb6bc431 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 63f86078..7356c6ec 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 53f1a19b..e4c9fa03 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 d9be7b43..f6e17267 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 0be84e79..16393747 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 92f1e060..b30e1e10 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 a11b0e97..2d2aada2 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 814fea11..2715a264 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 c66e1fa3..7bacb9b2 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 57d63d39..f8527c99 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 2d2aada2..481b6f55 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 481b6f55..183154ca 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 f8527c99..117486ee 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 67ddb875..6500a8e4 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 86707fd6..8dfde033 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 b69c7309..db95d33c 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 ed78a8e0..93cbe135 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 7aa200ae..e4081d9a 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 60478729..198b2aea 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 58a8f710..384b0f3b 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 db95d33c..fbeb2d46 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 97ce6268..f0c49e9a 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 c08d2e62..b730e607 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 1cdecbd0..3a383d9a 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 e1dfdbe7..967c8538 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 a6ae2a9e..e8241630 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 ac3ea46f..05ee237b 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 c29faa72..610276f8 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 6a9d2f29..43dd1cb0 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 2713df11..427fcd4e 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 a8b5306b..8f11a09e 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 807f932b..22881742 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 98a4127a..2ee423a9 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 2ee423a9..0c593c55 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 0c593c55..76dcf3c2 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 93cbe135..1535c7ad 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 1535c7ad..9faa06d3 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 1bb2ff86..fc92c367 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 02b4a0c1..906ea224 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 e7ac620e..33193bd9 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 a91f167a..a195fcdb 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 1d427a2e..97074004 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 97074004..95b7944f 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 967c8538..775bbd29 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 775bbd29..3bf5ebce 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 3bf5ebce..55639619 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 55639619..e692497f 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 e692497f..370b7e5a 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 95b7944f..b22ed5e7 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 370b7e5a..b5ebb117 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 b22ed5e7..e53bda57 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 b5ebb117..f420cce6 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 9984198b..f45216d0 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 00000000..f445ef07 --- /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 f420cce6..2c1f65cf 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 f445ef07..8d951830 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 8d951830..7354b6c7 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 a486128b..4c460a3b 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 499be7c5..7c4d55ea 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 d428bcb9..8e2238f1 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 e0e41071..a956ebca 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 2c1f65cf..44804c6a 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 967c8538..845ba339 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 9faa06d3..c3a8baba 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 2df80d9f..3b5af1d8 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 3a383d9a..83678ccb 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 e39bc7c6..f5910d96 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 43ee5bf0..8b2edbcd 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 71d353c8..b1594904 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 720c01da..9083efe0 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 594fe776..a4b6d139 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 9224d231..d54b588c 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 4c460a3b..61da5d33 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 3622aef8..d43a534e 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 f07d9914..0f7823b7 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 a195fcdb..8060deb4 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 14d221ef..6eed43a7 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 2a41fdcf..8503d153 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 e0e41071..b695a680 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 790d0168..6bdd0506 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 7c4d55ea..bbc50e4a 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 53369a4b..7a0e0b35 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 32d92099..342a29bc 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 c33cccb6..671c2a8f 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 05ee237b..cd132dc4 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 7dd1640f..4343d5ce 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 66ff3ca9..c9874b2d 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 ac688eb9..049d490a 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 afd6b4b0..5d0d575b 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 bf7954b4..0749678d 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 207be548..f6c43aab 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 43dd1cb0..77f64e75 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 40f19c62..954e3ae9 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 5e8efc17..3b4ee16d 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 0725d02e..804a8879 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 3b7201bb..9ecdd184 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 eeeec80c..ccf1eed8 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 d3be7ba1..f8a7a3bc 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 a4d0a033..5bd969bd 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 2d0fc8c5..ee661d9b 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 a58665a7..aebf8917 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 fad7a388..784592ff 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 187572c8..ac62631c 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 a2a99800..89861a41 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 93e90395..a7526c8b 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 6b87da21..0651fd4c 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 618b003e..599dfbd5 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 a32662d0..551352ff 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 46bf39b7..00f1f90c 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 c3a8baba..6ffa5ccb 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 34d5af48..5c1e1218 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 a6de0384..837a197d 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 15105ae6..2f10ac00 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 a9873960..c07b458b 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 fd9814cc..471a84b1 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 9f2dc988..df87a0ea 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 60b22a51..b6a9e5fe 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 384b0f3b..90e72cd4 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 6545f855..6a0fa910 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 8e2238f1..ec494cf2 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 549a44b7..7aa9a000 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 f0c49e9a..0a10d801 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 b14f3ee6..c5f3b693 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 3b5af1d8..0eb40383 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 b730e607..18d42d14 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 83678ccb..641165e6 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 338da4ee..c36a54db 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 cd132dc4..858edbd8 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 8060deb4..74c674b5 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 9497cc44..a060d6a2 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 00000000..99ece243 --- /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 471a84b1..65c38e06 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 705f4650..f096218b 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 d54b588c..bbcea134 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 61da5d33..2f56b6da 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 d43a534e..8852d602 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 0f7823b7..53ae46bd 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 74c674b5..292b2ed2 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 8503d153..955a3287 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 c06d3f77..bae1b5cb 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 6bdd0506..a257425f 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 bbc50e4a..6edec847 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 7a0e0b35..81a50bcd 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 342a29bc..9e1a4d98 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 671c2a8f..65c30ec6 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 858edbd8..f94669d3 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 4343d5ce..6ebb11ef 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 c9874b2d..a6825b52 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 049d490a..816eb370 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 5d0d575b..df985ccc 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 0749678d..dba37b5d 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 f6c43aab..31c9153f 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 77f64e75..b22d49cc 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 954e3ae9..56d14652 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 3b4ee16d..3d2ad3be 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 804a8879..165597e7 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 9ecdd184..739d5357 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 ccf1eed8..498131d0 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 f8a7a3bc..adb05c94 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 5bd969bd..2b062c3f 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 ee661d9b..a4a179c8 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 aebf8917..cd9f270e 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 784592ff..ff24baab 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 ac62631c..6988efba 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 6ffa5ccb..04d7b85d 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 65c38e06..2773a304 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 04d7b85d..23aa9535 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 292b2ed2..dd05dcdd 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 b7cb1bb3..d27b151b 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 f45216d0..9984198b 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 d27b151b..bf092e5a 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 bf092e5a..112cb53a 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 9984198b..5b5d3c21 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 5b5d3c21..f45216d0 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 7354b6c7..00000000 --- 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 81a50bcd..a907de84 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 a907de84..91607877 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 91607877..4b54ba8a 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 f45216d0..5d9dfe08 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 e53bda57..213da08f 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 5d9dfe08..8744780b 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 8744780b..7ea97ced 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 7ea97ced..ff66ccb2 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 49be934f..1069cb8c 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 8b2edbcd..f3cca9b3 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 213da08f..0d5b81b2 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 ff66ccb2..ef26534c 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 0d5b81b2..d819c816 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 f3cca9b3..30431432 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 1069cb8c..29b066c6 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 112cb53a..845ba339 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 d819c816..1d427a2e 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 ef26534c..9984198b 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 29b066c6..e57994f3 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 30431432..493448c3 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 845ba339..d5782eab 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 1d427a2e..abec5b23 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 89861a41..190834eb 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 d45a6429..4cd9ce3f 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 c141d0ef..774b6184 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 9984198b..2e6ef561 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 a4b6d139..ca6823aa 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 774b6184..2d77b1ee 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 b22d49cc..abe642d9 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 f94669d3..cdc44d64 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 cdc44d64..e37985d8 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 e37985d8..460b26aa 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 460b26aa..d2358a33 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 d2358a33..e3c1a1f0 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 d5782eab..99bb20d4 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 99bb20d4..cb32ac9d 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 cb32ac9d..0d1571d9 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 d5782eab..f03fcce5 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 00000000..6c7055b4 --- /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 d86c8283..02b156db 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 0d1571d9..2b0e492c 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 2b0e492c..65bb6f6c 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 65bb6f6c..0d1571d9 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 0d1571d9..466ad3d5 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 466ad3d5..e1bd9059 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 e1bd9059..393df44f 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 393df44f..a782de22 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 a782de22..84b0c13c 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 84b0c13c..a782de22 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 a782de22..08b1af79 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 76dcf3c2..e4203fec 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 e4203fec..6e84076c 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 6e84076c..e41a616d 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 bae1b5cb..5ab59b94 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 8f11a09e..fbf52403 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 389ae0a7..66dbee50 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 664f8861..6c58fef3 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 898fbe4e..9c428a00 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 156e0c79..46622746 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 22881742..ef57f375 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 3036ca9b..1369b564 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 165470ca..c7dbd9b7 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 835fd87b..7daef8ea 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 46622746..5708e35c 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 23aa9535..fde03901 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 cf6d78cd..13e9d36b 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 9b75aa08..d76b8c04 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 9083efe0..7707a41c 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 5ab59b94..6d94f881 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 efa085ea..95d84ba4 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 95d84ba4..f3a4bb52 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 f3a4bb52..efa085ea 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 23aa9535..accbdf8d 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 55a5bbae..fb67154b 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 198b2aea..a3532d49 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 fa367f48..8c8679e9 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 6a0fa910..6ea372b1 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 641165e6..061c3293 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 c36a54db..176b1ba2 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 cf6d78cd..5087538c 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 f5910d96..12d69804 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 493448c3..e9b8edf3 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 b1594904..ff6d6c00 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 1369b564..0b8cef70 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 ca6823aa..874cb4d4 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 9048db76..d74842d6 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 bbcea134..72f55b44 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 2f56b6da..14e8a463 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 8852d602..e2935770 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 53ae46bd..937990ea 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 dd05dcdd..7394a462 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 955a3287..839c69bb 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 5ab59b94..88b0ba8e 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 a257425f..dfd76405 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 6edec847..9c9860fb 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 4b54ba8a..6ec1152c 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 9e1a4d98..295104a6 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 65c30ec6..5604a0c5 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 e3c1a1f0..2e313e10 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 6ebb11ef..a280940c 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 a6825b52..1cdf529c 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 816eb370..59d26135 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 df985ccc..ee4b4533 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 dba37b5d..66373a5e 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 31c9153f..5a137f39 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 abe642d9..a7e42e0e 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 56d14652..c735cb28 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 3d2ad3be..6a17cc90 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 165597e7..ebb43f6b 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 739d5357..d9463318 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 498131d0..146e60f9 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 adb05c94..72993297 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 2b062c3f..a78509e5 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 a4a179c8..483ee67e 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 cd9f270e..459c517f 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 ff24baab..ca99be12 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 6988efba..53de4e67 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 a7526c8b..83ec5db5 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 1f3d5f7e..21504d20 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 551352ff..dd0ce2b2 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 00f1f90c..800ca35c 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 6ea372b1..62289d5f 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 83ec5db5..e60d4652 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 800ca35c..a5f55a05 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 d74842d6..d4d803ae 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 e60d4652..e4ab1b22 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 dd0ce2b2..ea3553c8 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 a5f55a05..a16327d7 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 accbdf8d..13d0f2e6 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 061c3293..95ecde6e 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 9083efe0..76795576 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 13d0f2e6..60e2246f 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 2773a304..0501c298 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 95ecde6e..cf7a8831 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 76795576..75b5951d 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 5a38fe7c..081db3e8 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 738b57c4..4e98f311 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 efa085ea..145eee55 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 1ecea7af..fd5b5955 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 8dfde033..693ae7d4 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 427fcd4e..1e9af30e 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 ade63f0b..d15f2742 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 60e2246f..b7bf2b30 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 cf7a8831..986d93fe 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 f096218b..a63f49ba 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 874cb4d4..5468f580 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 efa085ea..c7855c28 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 c7855c28..b7ccb08b 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 b7ccb08b..571f1eb3 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 571f1eb3..57c04e8f 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 1369b564..f4e2b836 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 ca6823aa..d92cfba2 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 183154ca..de1abd23 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 4cd9ce3f..b8473072 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 2e6ef561..ebaf8c31 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 00f1f90c..00d04688 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 d76b8c04..7437b034 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 7437b034..b7fa64dc 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 62289d5f..22700264 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 2e313e10..7b979aff 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 7394a462..a567877a 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 a7e42e0e..8103ae3a 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 5161f984..2b109c18 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 9c9860fb..ea2dbfed 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 72f55b44..64b9bb35 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 14e8a463..b95f7997 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 e2935770..b40f7940 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 937990ea..8bd3e9a7 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 7394a462..e1adc224 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 839c69bb..cbaa013d 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 5161f984..61349747 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 dfd76405..a7a4df07 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 9c9860fb..273760aa 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 6ec1152c..b50f9fbf 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 295104a6..c4e791da 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 5604a0c5..9b8fb9f3 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 7b979aff..e5689324 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 a280940c..04b19995 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 1cdf529c..a2d55bd1 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 59d26135..328f4c29 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 ee4b4533..061b97f9 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 66373a5e..8ae67062 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 5a137f39..fbe2c1cf 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 a7e42e0e..4f222291 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 c735cb28..609523f0 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 6a17cc90..c779ca33 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 ebb43f6b..1939f627 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 d9463318..91c8f31b 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 146e60f9..65bfc512 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 72993297..bd2d44d8 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 a78509e5..7e1d8c45 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 483ee67e..47e59e55 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 459c517f..9404c119 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 ca99be12..eadd7ed8 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 53de4e67..c5d44ebc 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 c07b458b..3feaef51 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 ec494cf2..d4fcd16e 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 b33a6dba..8344e562 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 8344e562..600dd47a 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 4e98f311..9dc2bac7 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 9dc2bac7..67c485fa 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 600dd47a..cc6aa2eb 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 67c485fa..e7ddba33 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 cc6aa2eb..362161fa 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 e7ddba33..feaefc11 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 362161fa..b26b6bc3 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 feaefc11..efcd0217 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 b26b6bc3..6000552c 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 efcd0217..34730984 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 b7fa64dc..f1ea6e57 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 a63f49ba..a5535c8b 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 z5C=rPzm9=V$tHhzYx1qXXs7-h*{Z+XeO5SxS`cnvyM|YR5I3*%N05Y#-bd)Y5ME6( z_=@Lty*qT@!fhQXE9W(B=2M$~$$h=1oI`uCe~;lcSQe?0${E76=_$Wm zm=Nc(gr&s4KB(e^f|WkvaFp8H~^fKOu?!wQ`L&H`3Xa}(ib4QvNE$U+$Qs0Wqx{c%N9J#2^1Df zj33Tg;GO{|1$?>miL2cRj=>TS%=(yxaE&=8*Jz4U^El#*d5yS~SiGd!aA19gEhLTh z{bm$7AEwaarb3;YcR~kyShUI2mUQXkEb5XZ4Wue}R#6-&=>Xh*1$JxX?^Ci@-Q|zk zv^Y<4x9}HZ@JGuz0B%B4PQa|TDT z%krdq7=>#T=SAs=tlj)OZueC0vsv>CzB;TupXVL1NDgiz8!OB+BiDxGev_|ytbBN- zSHiWH>5(^{2w9Tz;iY5Gbuw0!f4Dsy3EvSzSB~7;WiApeF}79( z1((T4jJ7T$h@M&Zp*wq&g#QdU9*7r9(e}T3G6}qng0^L-;&ZO34W6>-!PS5ddO~nL z*fyHS-Al1^%XPAHm=a&$nRaPu^Z+ej05T^~1h+o%cjJfmsfh;Xl!i_ml_^)#>Nq(r zj7hihX&lK7%Tg;1?ufmoAx;Ri(y+@c(widEP*Jl_AR+AzuCO`VCL2|JR{yf-9^3Po;jL?7h8RCU%V(@Y z81&EX``Dtu92xfI^Vz(j9C9UfzJ4Y~+IS_qk)@rN#pz6Ln2CKI_EhJ?05&g*q)H#{+gEz)mx6 zcO@E00ttIsg~gD2=PuIJv0r7z)%p!ZNxA&BsU*?4(}YPn{9AD$Gd7m`3opaxugZJD z#pgl3to@XH1Y-ug5#t|t!aagMkiR*UQ0t_GfEBgw@m`j#qD`ANgNl-KHF@x2M)NUG$KyXrCY~W9nRTj z=4DIrGXh+l!Wa2Rs*F{;>{9ev?{O3ne7K4T2~=?}UbjWLP?AcSn2C`mZ4(WCPi~!3 z>0WC-TZV1X{egDQia{?guZQ;!D!2s>dw32MjO0#(nV8k)b{k0uTZWNqjSPpYu3IMq zrc@U#ZG!kF4@94B8R7F1IGPRb4OJmZc+sv)ilwQtPE6(TbLmW`J{!VFjY*@L2iGWH zgPGVBLD%hgGK}XjABWv>qXBuabI}^q8Qh8Q=0IdUN1t6{@YJ-Onyg9o_ljPz#K-ij z2)d%td1;FXju3%*oZ>6-wzH5TUr00|t-`3QW z%pRDPiTKW#Ui6Mrb)UA-_*fwz(bhJwQxb6lM9?#&cG~rS!fHKs~3 zk-7BC{nL}nky(M0jefp^LPBNe#{vfJV;TS=n8ZWj`&H1baQ$eXW1B93np>Mru(x3O zq*(e?cgzKL`1DoC*yg&O#LR{b=LyN9I{iaibe%b0a25x8xOFg8B_l7zto0N7$<+tq zo;IJok{lA>zJ*J$vQcHBs!q&RuA|k%gpmdhWLccSm_5(HtzB50Q)mC*0Aoz4Q#&Dg zV`G)@9zCPgprwXuOkR0jUI8;?vU1IBMS1;^vbXsN=3%@We<1k>=)5;L5B^>E{0=o) z|6R36J-nMxK15F=sc;Plpdme&Wa97GSAQsDfnrkuM0MX#1L{#tah8cfJ`89lemTX$ z8)|o#A&qAX4K8GRM}LG0{PM=uO)N}Kb<;m*ypms`NzRw+{*4^kYW4a)upx#qbZ@0K z?xv};q)VvLUM+)2|L_-A312<;NKPmRd4fUhuq5q+>U_LjyVggykYjNCjM>-8-Wwb~ zqU3+Kk%Rioz3E)4@uZWGGG-uz6nI~_l(c9L(|1W0J&Eqa2TSUD>jw$8+&u88e^^Wr z{-0+5%_29+;;n#Axv!=*l}&r*el)%TDWlPWmVx!QU7Q-uL3IH+gku?v<@Oxb+c$d8-44WQoV4C=ZHT^wVtfaS0`$|G4@&G=T;_ zg2no@e4TkW*PXlfk1A^$1K2Vh!$!<}{SF4ML>*;l_>M zunj?rrU(&M`vI zYll9JWXgB^F*gQBhTo^VLinCkrip4r*%!T2=Q-utj`rv&oVHLouk{ZRx<=sT>1jE6 zM={lfG)*@yD6p=nu!GF_z)C%H8n|Z_{bqh+bMhMn39mTxar1kCk8(>YWhE@_E?q50 z>M$7V>b0}_DrCQClUM(WKb&c^+*c*)lt#+a|>e#ahC(wB0=E&9xDf;?~Tj)yDB-rC(x{$w(Wc7U)8wb=o{-8 z@MU)TQdCK1)&0}SoN>G-E4uYT6-y2rQV^UCN3BG%ue1? zg)~zzms!Hi2QM7m7-%@;ALx}+ZV91HMhl}a$0K6v4>SuWr&wezBE>R1 zidVQfiE@b*WY%|t*zoLUaAAOzob+cSU+W87(ydlo7JXdnF`15=E9sfeRMAA>?E^B~ zkChyNb!gq=N-H>(C6hQ%d%o|Cm+O1Bg$xFhVl4si1BflxiTd7dB`fgWY@%GnEqadI zck0p*6S*^uUwBH=zb|Bq3upT%5`X3l!gr6g_B=60YSLtm@Yg5h3WXh1eF8m~W+#>; zD!X8J$WX9vUu^wx05sVhfH3smpOnp+~zoQO)cS%8^(w!2boPdqTGp zVP^z>yi$XjcMk~>DBbJaTpri(tlB73jAQA+Tkq`x_-L~W?4lE@yY**|Pj5gD_cemJ zsg@4UvCsj3@fqLOr+e8Eb13_CgkbNHXMI4>ly=!Bclt`Li2XDs?ujHU^mXfkE^zeQ z^ng8`2M}yAO!V~r0Ow^O5X&wDIAXjV6^CdHMhebp3Sh1#RRyH48`L=q;(yr4EHZNi zQ0oK4Jl9F{mGXn8hz${*HV+EY4*%RMvC<1m{2lg~wK)pzN-QiJR&`5=6Tk*PCVF)5 zKKHCnmE7F0XW-Fl)|Ycd`xk%FiXJRW>CUWsgovhn7SzIFNvSCZ5sYb-uKv7pnH4$` z!_5V+O8E>f#>%+U9`|5FNKb65EVL{U7ANOiE-mlDaK|5|8o^r1j+>4Nd}e%_3+PN& z>jLiSg2))nnh)9X70dcl^CI2mgzpTrx72W-wP}BXV}NFIFG=?Q;Kap^$RJ^ulj58J zdag4o;kBCz^>T`kyMpPJ%)}V;8+a*o1jB=q>yM0bmQ4|(=^C8Z-qO7thGd7`!>T`g zkV~P;F;WeRI@8mx^08!F2hsC$&qAjes04d%T8W>>=fmFQ4d6t@n9s@nURDXA(i0S3 z*wy?zqUrd!$J469^giRodf~9e$+0@E0{P)7P0VgGx z7)q?rzJo91Zha5qypMM^67|;ituM{3ONJ1oB&;9kj?Sxi4rU@Md~K`yyhD>sDpl72 zig@x<<9m+feT7S)r_>7Q@41i%av9EbuE*UI!tsAV^4)#D5@N%oe7Gi6;Ege-8ywE1 zhX{x1rg7`J-u%VMCJZ@iOa1M*FmA^Rrqb?`ziH9wV5{L@4L{wyccfmSV1zR$XAWDK zdgA{=d%q-Vs1vu(#hkxhv70wpUh&s+aM#eka1X{PPvZN<)>l|!zb(Ah z46AgEc%IJkXdt#NQ)93pG}c{+in_Q!)T4?{&U^5%^}VvHBcgZ9_;5Yt%*t(^b+XUf zqKx&+5(4*~rO5$6kv?v90K8}*sQVKPltb|04EG~{1FR}BS10cN%HeoVKLNCIK6lK4 zo7Xyh3ost)*!~{4OS7>O5z8X6bWpkDGwCJI$WCkp18Vu;G# z^9%TFlSchmQ)wCQ*r3ZNaQ+_Owb2Wi^HYOhoA&nOXBQX%^!ycf%?ww-uuJy_XSf1X zo@7>dw$FAbDUjk351S|6X#D84*)&Qyt&?V_szw(SuivCk*S2%6sMtaGi$NuvnvSCh zZQY|oKYk4xlg|P{A}Va_8sN`%L^O!=3&guswdkB<6O2A(E>c*Igl?i~b+n{HLcBUP zSimX%zTH|iqyzh`c{TDy2lUGyc?7!69p3H z6eo*eBjGWnt0DPjNcNoL+O_Vm4K)_?*WM!RSG2H8(C3Hcer)uY@7A-3q*J)en8RxQ z?C=<2#n!^@xW$V6!th1e`A-BVe!vHcbS{82xBk_u9LK5(HyIK;nY9f_&7X)ODQL5!GS^q^cTS6m9XZ=W)D`=aV@p9yi4AzvkB@=4h4} zX$y;^`c*FX^4)$bMGD|L)0*cNM!ZhbJ3G^Y&V9{tTEuBaFs(aRzaItJt6ns4nWi6CI z4zf@ask{k+fE@I;d@!e_q}N?W(abPD2*|nhr>P(t?pwNK=BKCOpHYcvaU|pWy2CSD z!%09)A2&_o<*S!)<4F-}VW8n&>?Hf+pKF&(0)%FC^xVVri|Rg>{d#X6u%oO=8EKHt zxvtZmTW7wNH&o@==u@9A?yX9!!5I-`^wcpGIU+{3!?7k*Rmh=z^|Pj(02e3Om{)>y z08EePr3NW}@dQ*>`1>{TKxl->IFN{&hVeRjGyP6QdlOONtiWdrS}I1;2=#TP|Ln44 zH}hEtWQE`($-K)@e6uRr-9o?7#JS;cMyw7~RaFQ{NKpwL8J7<*(Z?)1tsm6X)PPAQ z{u71;puCUZ55NszAmm|_Tb@mVoi{zfM7D>hOBbAAdNyD#cmuIh9hLv%=`6gO{KGab zjYx?iT}p$1ARQw_1VoVT5CQ3K*g#5Y=|;L+nlT!ryJ0kvqX!$?UViU+&wI}Pf<5-(F{H#b#+ysYpub9MahZjSxdmDrP)^>{@eO@p~si zyQ~JjV6Ca__-9nLxCXePlTE|jCW1RqyVJWjyQ#;$!r5hf7{lijSZ^gc_7NVJfA~b5 z=rXPMyh`jd2DP@uS+Tt?|0t~$>W(CwQ6#@uA7Bqw&R}iwD03jGr8tW?igqBG6y%cS z7~t7v#WEM0GA+jM+Y55l3}VIgsI-5=uk&1Joh6u=-}iYU)_I^Y*iiE$ZeQxBbWU>T zlQ_URp)eyblwCc$$N};6>OW6$5YFdQL&`JIb!HOGiwl3;;+KNq%g?KGW}DQo zh;4eW^B>`sUzzj}y(|fdBNHOZ{9DtRFUxT=(s}`&vQrgx18NGL(QS# z<^Ym~WR}|rqI$)S7=E_1T*6>TxzR&q&zhGgLo$i?-jw$;Vq6$>{{A)4zv{s&ZdmnX zQ~yX@y^1Xij21X`frY-?~SB9WT;)3)ldeBu|Vb@$b;zPPE$WV$r- zPbO8d?u_m6Q*$v@E|MBa`UGr|MwU16R~3&cJt&XW$$mUUniQE=MR2d?yn@s42!CQl z#$fNV@ps6kANrLy;fK=8La>7#rmP=D6zsys=Bk4YE~k%gp7|RtgKi#2AdY)y#e7qy z_g?*(b2b3`D>jo>)I&FxrTM;V~kh(1S9 z-wvugVu}qoHm}T0LS~*dC0@2H!>az=#<8JS0jU>gbPne0JM{6NR;G2TTr>9%N;nTzekjBINdK?gOt#UA*-7`e{$pkmyb8xh=Zzn= zvffU@=$L=H`A#LL8k(6DZBgU4c3(Y<`-iC8?!1|NHJc`_s=Q6z^k2xcEAwSH&>1DE zU1?VI1=+NIJD-wSM`Vf$&gN5vr~dvb@Y0{18fok01ku$DK8{m{jj9 z>zPxKbO&4f{a7EzS=)#S04$)5Co^OhRh}ZsoGB=y{m8{d{)ZR4FqPJeR&`?qq+l!# zZxVFVL^aVc{2XrZXxO^ySLlW&PD4$^+ zF6~kq{=Xt{+LrGy{UH$i_DSI<>x@Qn*9b-Q*=u|hE&$biy&=m-`uZ7q``oZ zn@7niyO88gtj+q*!(sU^k3|e{xikD z)Rl#@V+RfRyyv0mlafrj2lvhHnyVsoFadbztX$Y_ivF?PPQY+q5qKFw3F7xhZol~v zSAh=s46M%uuw!qf1aUtluZi+UR%K-tLYkrBbN<;cT$9WQkF~gt;4--PEPZ6o`_fDa z1l10+91ePscs=Eqt9NUT2Yu7OTcPKD|2dXaHy#i)4{T`qIOI)J_{+11f(KPXY|F=4 zv&(#T1PO(FY03>y^c1z8wSy>i&1F|yAW}4B7!pQ|{)UQMOB_^JBmX8vO}o4>%`;|E zh8fZ(JSt+v^2o0uVX{QT;1H}}53yF4Q%r8M@$2ICrGSPXoPQO6u>>^*Qcw{`6+f|9 zT)W!rk}I~TyU~l?;P>DC`G?B6trG{n!@nNRZJ_2HJ@4-h7MHX-W!yg&%?M?^jr;6~ z&<2Q~{fBDk`j#-|U+tY~>>U+(ir-qB!K8a-i@ZLXZSCY@Ib;35cY0)mQi5e0}siqAY8$&nO0|4k;cWqF_Jy9dl_1^9ue)|kWUUXY>H@SueHY>+a3 zac3e~#59%lXX9`ROLQ~0+Hn2xvWtE0+(#N5jOGp8=3&c{SPy~DWeqo~$8yZSjd3=6 z91L=dvPF8(?<+xW)mMAMtggYk=+K7=WqsJJ)<#s^d-xa+%?CYb4?x7<8N16yW3wc0 zXwxR8i;%SI7K@wTg2QRCwPq6v?)EAfi={5xrF^F2kIo&4_+rMzl=ZPqnGQ?Yg8)-Bo7_>z;D_gbwT$Y^Oz&1)@Z-4~U?FWJi1-0nK+LOfcb z=Le>G=wIkJ_Y4H_=6Y<$89l3@yh9IatOn{=U64^c+BBEz?~uKIywxJJyYqpG6- zey$U(zx6<(sJ6pl_P+@v{AZO6eK_r+DA9|n{J5x;-V0^1Rb@}zB;ScSZOy&nA51I1 zYnPd@F)lo!fgLObk8F=_ii<4Oyd^UClvCR+E&3lGP40^qTQVT3RJE75_YV^7qtzl> z22PqUWyJB;!SABpvCzmiYzo(^AwJ4fReuayX!#;P#`@FMdcncbnJuX#yqH33Vc;qt zJt5iA#h^ZwIGzzpB>{WM^t)^^VJI7>F!#}9AY-^C!4{I*>%AoY{evlP*+Bshm5WjUd9$s8kylJt9|{wd5@YK~5KoTC@%rMz-b zyIf;GUDxVTToPFDkj3DgQdrVDeF*2!z;8$FK-Wh!_YTo>z#tjchoVW1>mFLO!yVU) z<9gG7kbUf`jJfP*DhS-Z6Rgwg(D|RSMV<{@Siq;I975CA8Or{OT<74j^pCn%YQI+g z*pYJ1Ju+TwHFlz5hav;09Gy!?VZ;nBl(ECIVNrJDHhcWuf3=~v8^hevjXLAdAV zp6RTrlc4Mx3S5>W#z7*Ah_@2rI>~5g@^kWX+L3|^y?|y>$KQUCbu(Vke?l4zGTQuT z^f=P0R}S=VLfVK7kvm-4;w=E2nCn)kG#twC%>6jKeGEk?2+i`3Tq?h2r`X}CfBY9G zdCpQk7)c;(`$2%Xvy=Yx+YEE^Jc4PR|Aa2-7H^vPyh@|(Dcb9SUK}*V%64|_=X_x4 z;N8$B@PkqZ6LaRbd$H2zZFjI`uR%P!tyaE3`*^YU$b!-&0&`hKJ?X#lq#NaQ3+%CX zZ#nKg48ZgzeZ4^XZ*AMV7&qDof9RSqp$zZ_cj`hFC~L`H;GB|P5fmZk_DkDfs&YUK zc+S49Oa)$s5#A;O!!QAE2JaEdwxEP?cO$&}y4Ku^o9l=PCezJ8;p&M{qlYsfqmwog z5tG{c1V+(YHw)msUa2Me(%ZbHqKD8<7o?YIT^s|1-uvEaUmN;BCXB_fxxE!cf-X!z z0f%XGB-?5Wx5^o{55;tTh%)%n=>Qv>2}*D8FWC)T4&YY&@yVDNUGW%uT7U99y9;tiM{fGZ zQZ-nAyhciI5L<#Be~05how{@n|6RSB`Ps80{pTA?*oV<;|TxYIsTlz8OS<_#BHWhCB?;{cPv5zWqp18ZG57 z{q)BvtKgScicDms9``1r0^|pb_X-ow;e*m8fplueTxwzbi`vm% z-cB^21tesB!)6`R3dfuv%xqk8%NPN3Wc~{abOEayzF(jyR}9P>w*L?0)VA*$0GsfF zLtn&!q|ng4ZIIcW)EWw2N_#><^8RWvTM&r?46eab+SzX>(Ss}5N63Qf)1+M(%4ZH1 zU0&p0op6c-iX1{-RFp5uFp>0vSZbriC?@IVR~g@dnFfM2qJ9fIMP9XvWB ziS?LxaZW!IJ*`BmYVX#(2F+zo9_Q?0OD})_s83R}ybB7GAnv2u@#4Gk5r(x=-;6fe z*#4nr!>GDAt>Z&8lpy&P&i{U;I=DKTy?IOhaEES!9^{_oipVB)p8pcw*_yMwSC3OU zjOS;=PdE=lLuF&GzKY~|7fE#O+Y*kesbK?O4G!48B*C3=VU!8fRB_GLrs#YBy}okd zXH3mPxe9Zd54piL=|6&d*I`*t52(35i$?a&g(x41`xAWkW@=k+ z)kT-d0MFE5m=^GLl0Rv{wFxY*oz(oGT45PHVH8+zhqs5Mzdc3%0B-H%;8-qWB(MFI z6Tn~pf^$nSm#YjClgy8_=Lky=k>E>{nKkHN)4Bdm=*ar|Q_uqZ55|41b?@}gg!y$8 zJ`}mpb}J6A3ONZRX>%tJ+;8&-LcJM&LtDXz&}KM89}L5=`nU&nkM2xeA1Ob9jDg`W ze@OG;8|xdkCPc2%_b&9ms_v?ToDI7(Z|1gU#2x{-k?L!yXy z>`wXlwcaKyU^ct2cU}F#ckiF8T)nj@Y<9v%WCGy&yk@&w;U5Tl^-*URIg9BGz(_ns zKin?O{?<^)B(TMYgml|GL0&QgNn0HdW<`K}{=PlIpJ`jdqiv-Tmi;bEdzBDAfdA3~`1$v>2e8R_jS9_Q%zVNW|UaRNv zwp2B^m0oTuFcjl|;%%piJX+aA9(Z4-=JK4vavgiDN&!9i&@$8$)GW_{IAD(j8!-*OiY&>M`BZ0 zFqu`B@SmfYy8dAKBL#_Tk)19rHI`rF2G)h#%)eM=r5+lyM88g-g+Dlcv-;7Gib0{z zH3{5|<2h{%!pMl5e_4ti5PeIE+6{AiJF27AeMm5)!|bf>`G^IplBj>*@t^i?6<@B*V_5lE`>U~yV0Gp8{-SKtc^Dj zjv>%7b5BGB$`OY4C+$S9nQd2}8z4JjD7`hL&@>&;;dn7{VWI_dG~J0D1;f?BFt{>& zHSQE8e%khW%^kqlq0V;=7&L(gqhT$6>n-lN?Qok*_M3@<_c6Yf+??J()0v%Ag!ZCY zRqit=Dl^dw7v7k?3Zm9qcb#p2ws>g8e2Z4KS)kJvciri!Lt}*m#Wf^CQb)vJd01cZ zspyWsn?C@XtkbJwZ3D;#XztkSPX_ri%!R1X>p4CyZNjHE$VsIH*)DVV-}d?!Dl>&O z0vE`Rx6?XXENktKOTP4ita|ey$pG7zsS0Vi>@11o*!R<^ITTbpk2pi`h0BfDMxHOH zu4$e=YEmIh`yLlT2}=IxQcQOvVNxQ*a+WLOK2T+<&n5xb-|KxdKOAtIHvbYTj!!rH zLmTo4JBW$k$StU_XZWOppLn1PYy1i|zHp)PAMqqUmiqZ>Wc$l2!K^~gzc zglyv$du}x-aJ7@142{qsoPOFOftxS(ZP8t)Q<77-iZpijnKl7P#tZ*!t}gaot7=i3 zk{DHubN&pDiEP7k9wIB}C%5D*?+xo-%9G|~9l_-%-})HQJ+omLtYHI0lwqd;$pr{i zP78l%gS09107k~fhcyfbK`@|br#eKc};^d z*Lk*Pgx>)dfsYtjZGX^dqXC7X-VzNam!$ds!s!FPNWF-;^}?K04HED%5}4&q#N+S%D=(rB|=O|ip0_zDmO zr3*Ybz0cX%f8#ohujg-}jgbYBPx_vwess@r7(+R;=++QZ7>Cs8gi-rL_PYbed^+8s zk-17_w9YyeK}aCx{G1ICcZ7+-2PNw#7_n{Q8=1{AyPSuxo9aB;fd$a_JNLptQ>de_ zPsg=&L{X554Vi8vsKy@>qR;Jc>L9nMCm}_6yM=N*tECNi?MvS`7?nJzdOZ`q4PuDM zITof_X;-)NEV=vk@stDz4|x;_LoYgS&0%E9A>PK5(GUXwVc~9_&}}**ZeMOh@Q}di zy!8Y7Pg}5aQ5q6%#4d#%JU%zWoAEd69n?9M6A#2(4j&O-KYKcdQq-ZuzVK!1NpbVh zbF5E<66le=T?*C+Bt4NnQTKv9S>AS*=q$V|Um(?aEFTM%gg3ugUCQP67E(HY`V>}x z>Qh|p*|O}88ABQ2+QBo5vxd-PcTCKuGf-KxTz_WcjU(vXWSF6 zxsZudRF(ai1yfwyt-fTN!Pp%gpiW-51Y6x@#8*HI}Rr zHTrJe)BnXD`7d8?`&2b?4;_iw@F5NcbQuK^J+vr%8GqaE5>CSaSXmRQ6KnHv4a+@RoB+YMVcfaw<-Cuss_NV1#!cD=3}_(O+AyN|$XTR1B1rzhk<7QYxCimg^?}1;F&z&EE?cL9NVaMH8*V1eS{ybrB=4K3u z>jrFE-ZIu_(j>!~1durJL=uxG#NR(oAmMV*2E%5!&@|kv zXb!m!!DOyrP$aq?2!Jz2iSdue1(N5}jV>B7^^f_rXiH^Q-|ay-r?O~gBq zwkGO}ST;spf7c89T{c!e-)e5sz=d5hbB&s|p#oA>_ktl)Hg(~UU!PZQ^y3NRaI)v% zjfS5MeVDE+TW+>j6z9HiQBk?&?S+)lz0Uq*vXK~=`D(%P735^8UQ))$XDQ3eO$wgh zfT}NRY-pKZTVH$oSqgpAIJk@kgU$Zp+_gnSJRoYwghvg4@@P;VR!}_CVN!FfImbWf_C?|bt{vYE*Mp^BR-pZFY0D_OCEI?6vQrXhLE@(336ix#KQp4)7tPj*fIarZ^=REtyUB0agv zv%TH7e8S`&0_17-09~f}md#PEt{d#2Ua;LG$Hk9NfH*f+-#?}eP8gqIwpBOEf4>g25B9)SEj#QS09*1ephlszVVHFIf(HAYfVmg zU8SbOVN<>D3PMO(#L8fedBWkItEY-2`Kvw^FN|rYaIGe=?$0Z(J`trosOKxbk*6~g zo>bP#+g;+Vwk0%6u;AF#RnVs+86PMX%_Nd z?;t*z61=Pk$;by@Zdij+l&&LwX~`0>?SWudFR8}v=MmOLH4ewS7r$AlsZaCSK&Vd5U$6u(*w$Oi>wAGXAJyFOF6JgmKT z(+%=Qfa*rS4A~fNk#l$FFzHO)KE;5$B4CG{R6;qkfhliNrUupE+)O0T>&$Qs#B*0T z@%IooS~ra`qamyIsorOBgwuP;6VGtdjW#kW;K3J3_g-c|%VjUKxeUSL^C4?YyVF6z zNiY5l$^2cW5(7T`ABDtDGg4v_W=eAsHllf^ICmR0#^40mJy-0=$WX)9H)5>u0F+Y!>9PyO?SV!xwDWk0@{N^b`a8C+)Jl)XFR#o2j>JP5vg56;KI2Tb*0Gd*Sm}EIBn#Tq z=$Yi`eW7u?1vA&3`^WAPmOU%#%FtS#291Py$beL`ec>3`UJY!Ef(+ViPQ)z?1=FcXr-EI@c_z$ynVCGvIW(HzNr-u*n096`HP^(?Q@yYWExc~rJ8&A` z_y0;czJvkQqo6sT0!4OT@92@tnjknyc>nU$$E`ozyO^hXAEuc@{iDR&WU*l4opTfH zOz_fkYN-9~lN!(Uez}g=&VFN``)aWSs5OrDIN8GE@FKC0k0=aYrZYF@<9VuR=;6*`P`#g@k`ZP zHHOtwBPsh_nrI~eky5Fw$X-`Q&n)oqc#Ax5 zh^+-ZI5jf`pUDJ1YHKlS-3?UC>aLx%_|5c|zun>eW1Z~p5P6cz_xhG5t={N0A+qrJ zA5|;WMZ+q*hhj`Sur7wYl83RtA)g(Ts!W+Zw`#+NpJ5 zVow$eZ6oY{$!X3F?u`+|U3OgX(fui~Z_Z{O}t)t2b}LnqHO#?ux|s4ET-I z-Sqy}xTMT+Hi-m`PLMjjxr$@TS{l1rCdAi(zwHY&OJS0J9;BxPJA%=4-}!VHdm0apA%Do${FGZt*Wsus^p;E82iB1Cjb)66F^CnbWlF z0+)1~jvP}YFH&T3p2FFKRzUilC0j0mgcS=L`TUMNuT9$#Jx&ATdq?M6P3Zy6lff8b z_d67!h2vf)nGnYA+RPAZV;-VG_u*vlmN(ZlvlQ!5^}VJrBMvYk8&PLrw%p>JH@H;z ztmSG!3)LQEys|cx0$8;}9A!ZE5#Ihy5B?zFFgkmMA?5w)kpsXQv3-iNkBSw$<=7bv z$A2V`Hh%7pADa6;8&NO7pJ?aS%4xPvJthS*wD%Xy1!(cvZ*lX+6?o4qa*M-O z@@a53Xnr?5nseh4oW5kME(2%)xQ zd7v>nIc4eZD+`mYd{QD!4eq>+LM#nUIX&Zq%SWAqqd<)pdEb^feq2??M)E$NVsE)* zufgUlwO}ZG64^}W>rSFRtgX!{X+{!G;2sejX?>+JltaYT(Puz6#Lb(h(LdaKJ5|A_ zv(nN>BJ_@8rl->6tzKyTON`Fkl28ToX@hX7uLxm1(MtDHo3@boRUWY>-u}PJ=@O|D zkEol}RqxAM-ht)}{NW$L#+f4-mkCE~!+P)K^7xE{8_rA0k#5j%&GjL7DgtNWo@+xO zM<7v%%~7NM?x=yI73pwCqtFn;K#4a0jqS%epI*O3F=|yICt3`1fi_iBM}Mjn%??|? zMp4xDPlK_x=Bt9991~AV(}Y#lCW8Y4nqsM>`o9aWO_x^xI&@(W_vtvL>%~Pk(bHns8QPb_?J@!&b@VLx4+A#vkqaCh*^Q{ zRW9j)T~(e&&Sw*E)s8M<-f;`Z?rXOB6U(wNG~>~4Q~p~#d{jYkqVEx}vC!O7;6kq1 zAE3w-7A7pk>bVV^sv6L34vN0z`}r{zQPEI=swl8QCyuQFJ?>^Y#ibgh9F7lpA^(13 z7#7fOuR#t=^pWHj91-Bycz|SxQV`riD6i#It#;<5Knc!}Li~dk&k43({elDJqP|y? zWo+ywKzJ4P3jW3Q@Ji7btQpdk?6tLSY(9@!b##tib8U7pYWO92;86^8H5_~2Fx-Ln zcDO79VO2*g@yE2iE3*5kcKdv+B}0)VSNCGd6W%Lh*pai5%%hrMsji4Glr~`wt-;)M zdt8%v&e$Y24bSy~zvXC=vMvIQrDb{b_Md4NzYIk`K9l+PJM^HTagRZW{tb}+H*sTB zQ+-R@?+wwhcfUF1G6i=DE2yI6^q)7+7|a57e!)H$i}5UfX?i_w{oZQPdTn`wD=0a> z$y+`%`CnlW!7F@x{#Q)awbL89h&XWDWgWd2k6zX1U8 zGQnLj7I3Lp9(d?!Q0(K5u1JfhuL?tuoU}a-VG~;Iqj^v=s~z0gG3$7D;k6>n{8B&U z!TkcQujj$fQNp96^%#%<3W~6SDFl5BxH(6eqxh30TNZn%gCC@vtoEmZBSuLj8U@7a(z&Z1?^)N)8x2AE3vv?n~shPPK6HMIbaS5z-Tx7cc6tOrw z$!{bDdCt2X^2>rtRfspbZG+{vvrG0;jqqyg_<9^N%;dP|V<#hr&I$i)C8_;Q-x9r>mjB~YP0pk&L+WF}GHvf?S2sgx@?>_S`b6wT0;`KP zW0%ZhkU=t;@?nsSnee4!nd&u2jn1V`Twwfts#$m#ch)Y)4nH>SBf`6=R}+2dAzkLr zZ}VluZikuZSH1oH+fRc7UV8%|r%jkccg)6=MHoNvYL;_*$snYnM+Q6a`l3I*{pPP3 z^7NS8A5RRqBIT__C7ptY+uOAgwlcfTMU0E;`TKRpT7dkIdnP2o^{6-l?F&sy?ZV9@ z>C*ol@wZe2pvy^b>GP~~(_HFBupSoh6;yyWP43YMS%wQq;wNqeqJZqFF*6>H;f!p0`FINVW#TR$R=GE;<^ zd!}lE&3`eD1Xhv^I^FhprJJ02O&J>uo4tXfgkIGrmeg8@+M7rFI$AN`zY{9HDZ+Cm z;!J5eHH0C<8my?(P*YHGuvi72l_1~3newj5nCMO|u`T6-rqq!|1O4yc#eyuxf*a4_ zY++Fc#Zq}PE35>o?kvvn2AN5gstt8*&X)JP83%bheG+^%-8hOPmUr&Gy!zv0({d}W z7K6vhV#fi;S|*0;$4AzUbE(M%(>?`8j+qyR#PyY9-%x+vpr2PKfu6;fy|#v$helr5 zE(|AHQB5o-KR{wjqMEkD+s5H;0cz2T6P7690*Rr+T(WF=q89dywAEB6n4;L{j%rTP~0!x*YeY|ys<#6*I z#)u|I6v4n=*u1ikSJNBDo!F*uuh4?VU}{FIOUuT#Z@_C*LE^vLvz$)DtHtp8*a1Y& zOe#z4^kFOS70_|h;jEf}{tf=Agn9F&@(I=Qw}Fg#>`Yi5=+FVxAJ!R@8k0Nf@f#w0 zJFjlVy>IxtY!6%VJaeRz^fsIHq144CE*L$(TL+lhZW{l`l&eUEwm$UssKwAqUWF_T zqOPloLFhq&1`u)}W}Ka+GxN$D7U=*gKtvqA>@H#wsY8+GMC(uRTRTl0eyppECBNAJ z-*Z*I@;cVPiVB#TdjWC_$mV^63hpx6Fty4Z6GsoMA#U?&ol!lx3^~gi+h6QITfAQ9 zm@+cg^V)Smo3O12dEeb@UhA$OKC<}oaMTMX6UhGIOI-R()>^fhi>`bkI*SQ!BG>@p z!KihgS+1WIH9E&Ez`(!O%FXc1=lQ5dxCJ9Va@tt?S^5xO2N4DJvAT|LIzRX4C@c{w zL3NzaZZcGyFP*u9!ZGQj*R6|Zoc*@XL=p?9qP0ucI==o$snTP7{KXe%zfL#GG$cne zqAxyLy%mrR)>L%U7G0wl20g2exsV>)*QYGs9ZDLsavKRd&yU8THF{QaP{z=HDbWJ5 z3n)nA=rfB!I$ZsjFvxhbF6dlv(X2?{XJZwe|EFbN1Gny6aI4hBc+}!k&B$$*bVHCl z9locy6W7+20u5_&fthxzu^-4_$CJh?{p%MiNBOkpHZ**I+-gem8Lq0$;X@U7#wc!8 zom-{yGya0n_lhVs@#y@IuRn!0W9joMmA`rdNGNJQ6Z?{6JM5j{%v}@uLFo<7$kttw z`VgyG)WwIpU)rMcz!Yl_+Vo(Sp=qa2#&Jz))Ab_1>l=D(8DiMCHP*FZ+rsITwtJUb zt)G{5YKD;YoY2y5{$8;(1MJl6U%_K6Cm!G*9D^qFigw#!AJtN}^K1Ho8?py)^qZuk zME~j&IKL7SBX%uD)cbvaOugS05@*_uZ%z&pXhfR#KqrZ1xmd7_78Fe9GUKd}8M4M8=g15lMU;)N=5Bkh`K^RH1KH^$ zx&SjsIyyRR`zS?mvXQ5s2CAXeRE#*N5s67q1x{f49QgGshi1v!Ly+M5)!c1Djg2IF z$zt)RnU^!%+uPLtc!7X%fCiWa{k*n4!e2D8qB>ob8=p&joELgu_yz~7^V2SD?TOGo zg#68EP7A8B2xrk#lm?$s$vM~y-speRRIx+$ash)#7(`=7_}~v>?x6xqbN^M1E%jt?Ep{iG(_l=N1k{6LUhAmDj+Za-S{v-wY-IY#0OAz8 zenY&pz_HRq#pPF7&HHnITJwoUTNgX=5Tfa2ns4LYhq`Y6CP^3leqt|h;Ml8B^!9Dq z^m9sCP94Id%sM+@b_Ax-sN7XfxGk)=BzpEko^M@D+?H&22kpnY5KWHH-A`;bXSKol z)yvV8FXgcQ)M`AMo2(OyV8IzS$z$$me`yjG9vj9Lol$M-o)Tw%_QAH;o*i2%u7yg$ z*qZ*Qc>U#b{)+;wpHVM(HjjK>@4ct=diN`b%xcPB!}wafr2g`6=BvLix%yI^2yMzT zzR+0}r_hYhxlzodmG)#_0Cp^gU>bWa{TIc)q^`<8I8x-ci1kyHk~Cu_|FZvmIj{WL zQjwu>e;Z`EJ}AC%n=L_ClBwtcBgP&UOtEEK>I*0q8+Iw>r~6&}P7_7xnYk50@1XZq z?TMpgV+4>h6f5l%AyVVv`se3AZZiLh9Ty7>=|nCPv{Fi2lj}Os*>6so zx`X7FA{GZK@=bpRek-8utZ~QiBa{9PbwuVHkNwNH`s>Ls+=b?6^1_(qk6&;38lPI5 zfniy#e~oC~ng-UH>#nWSJZ+EgRy}mT@%s7JA|B{kJ@K*EGxKM&hD1`c&sAXa~6=Srl z#boY(l1X`(e1&7BKX5C40=P%QrYf&$bcjC_2fM>vzKfXLkouDweR&z`9+luLz8m{k zF9D*V?!aaU6Od_r$rEaE5QLrD`IWk*2$I`jTcO_6PLzq@g`%r_DuZYU&V;dD4o3e@ zg%74auHApcCwDMu5bFYYvha0UuSwx-Kpy1$S65Uw?5HQZlpv>|Bq@<)!Z}2CFp(XJoR^dk<^YbkDD3XePajCl-Ewq#hw*g~Y z1QcFQAtwJd>c%`ptaPS5+77YrV0csUUHa+lH`jIJD=n=V?av{3J)-5MWfohNjg|>z zG`w%e+|}H}Thw*(qLjsTo^R3w3#ZI1X^*S#C7=AM+z>srgyK0gu_EYp1=w6;4&ITx zB21KBOd}JH`$6-m)?#LF$X|!^b+}MfNz7|N%-zGCwHt50%=iag!^eI1!kKW7Ka~=J zJC|S?UaViG#O*`}yxGaH=YsTKEhkuBa}K{z-(#SN;cumH&~WA)AwJJCe@{JO*Ev2v zt<m|3e)sAjof9;$a(__v7ZCHbZ=$L200ou8B*Z+4kq;a#Jf+d8n3?G z>kcYravh1za!r#}eQTSK>X>oBFFm7hYV<-{@u7d?hmcZ|~N@e2g!#7oM?jnb6 zBD<+YNTN{hCIJbqWSjZXA4NXD*spWayqlO1HgT*Of<*5rtk#VciNp^_`Mc%8t?jK; zt_9>X>1NIL6Bb6G|DiXl`B3l zlZSvzuYYkQEYrHW;W*>ep?52@z|kV4!+@7`nRos3++wZ;S9ZPl@?jv=_?sWiaJp64 zoLy&_=Z(>~kUG=4zd58kanKGA>RGepbVR|cz&TU-_H0CR$!+0!IBdd9$GId!mv(W& z1Vl0c@=!Bn%to3>OVj7x(IHM>__B}f#X5V&V~$ny550__mqH9X6J{{|PMOYz6%hR2 z(P5ONSrY+)SxfEcSSY$#$TV;Nwd!Rn%wI_lC=Xnod?Y(GK@{l9v^oRf?;Y{S z04crp*YtKD&~-SBNU(GnBfXh-J*pKNr7pdQYHMqEUjy8u6s?fNqp>=FLAy?4==I!r zNV6Jl?h2@9wCCc)caA?cn}+zk5s=F4^{t+?vyGFDQSu8N(|5^K0rTWgqNZ2T1TLzx z+g34$jvtZJ)&lJGo9Mcs8!wF?_lo-EkE{-E#qh)Hi{N#>ze?*iso~e_^MjV&6qK)L zTFF24I+Xp|d}s5yX_hb;Y2+uwAP3yLU~T!#7t5oydU$ITL-WF%k(l-cEFg>WRTP<0 zsJcZkYW^&(f9mK32q7&~{k4w4+T#LzTPb4$EZpXXPTVyGe7xTiTLt*i<)|r*z*flB zxoy&OtI`LOfmDL=jU6lQMl7$RR7V_D-|w-Iwi`saW$&~!U9~=I$1~8EIh=D_YB(?* zEvO7rTR0Y9?R85o)v&QPB;baOom?(k&(F`OT1gwcMylLRUsrK%-^By@Y@!#2=fQ?$ z@GRZW>)KRaubbW_{^f1z7d&BwR+sYM&wFfIg>=6&0Nr)YDRE}f%;qCu@%-7+^v>FM zz9g1+JTFHa4+_7*Iok_6>%x}D9dT7d$oJloIJo&U?0m6mzWO3XF;e2F^I|z=q?3gu zHZkpImKoQw)gghWhqI~kh!yxiVO~^Senl3ecEDkfjLJco$=suzeT8y`M~-yQePW4( zj;>#JD7geQxSaV(%E(N6PqGCPyuP1O_37z{lgWfE97u8pl9Ii5K?VB4RWLVm-lxMm zZWwmjA@5>6e`t<)W?r1}%HoL``g}M3ms*u&0Ji?!JAx~Imy1EI2kwbyyQBSu=bovx z9+>7t?HlB*dAIIC3~qyoeHPd( zOfQd@Lhl_6ph+dSvpJ=|x+;Ekt+}T?{y@~*$xVD4UehNWbZflHCxY?4YDro0j8h9{ zlzHA-!9iw*S?>OJrW=EQGl%_qII_c*XKLvOonJt4>KPjjr_ezZ{+WnR^`MCkP#}?| zqk6B)#HV=0>FU?QE^fNkQDzPR1vZoWsz$RZF3F z$EF=ZQAYuv?S$nE{q?N%%0(O6QuX|{%VoruuhtBVz%rd@3o=VrKL{o^c%Ep?PCG-g zO5be1q!u!t{cxa=u8`&d^wKw(L%u;IMWf(z7wO~kCpUV+Qq^YZ9&?#!O%DD~bNUop z5ZalZXDK=O?>bF8Z_Q};YK*~C`hHx{PjRT7+V4j1i*{+JXZ~ETf8sS7{XX92h&sYk4saA$>Hgy6o|mXKbxw@$%szk7>W z%=eB&UKjomv+y?849N2cMJZT$MZ?+O4!xS%0L2lFq&KOM(%cGVG-1APJz};#U*_X` z?ge{=l1ZAT0$N+`vcPEhd#fCBg2Ud^SJj*xQkJfLengJ$ElmXzMS^VN>BqkYrdG|s zZ%0r<&61Bjy;|d=2^A$$9zd7r^EKGW_x#P_tbTyt!r9ncp!ve~rD>WP@&= zztmniE7EY*vt%gjyexMJsyjLE6I^_=H&gQGswL#!UZ%=q0R8KdYUcf)GO4or1JeY# zAN)xKj$_%Oz{n#mnIm_qV+^1IT~pugcl`?K80gb$3m$*F2Cy?`g6r9sT-%w#+s|8x zLe82wu1}A*8TZ$&?5;2V!CwI8$G4kjXna7nhliJXfpPFuny!5`V*a`_Crs@sC(wV# z3B6W=>K{=Ivme`x1(h#2ug_wn`Y|Am<}2yDKH_7)d#5i}J?+A8K&Vw|nR^}n_J|kz zSnCsUr|llG1%cB4(3Y@k>CU!GpxeTJaX{#O)`K5{oPvuI+sx@O1a^ZT)4eMU;(Pt3 zivzgBoV7wLN57p*AF%lq7Q2#YwiI~$BFmbkw(0El@`#U`c-G}?Y#)8P4{eEn_@17k zOEw%`;Hj88&~2t!`--8u2rR-9H4FW;ZybUG+t{FCh+22!o_Tlk%f0d z>YqPyHPA)y&W^zlKdC6jdT?HDz3rkIm3a4rK=Jrs#sxEQTI~MGpv>z(^Q8p|%qL@9vZAl5HaJSV0 z;*9!I;rz5&=KRX+s%`IJkAr`8hAP)*>eW`0XXcOFmwgfLT8wNvD;LhTwySl_tsnPV z>rOO_od#@%Tin8ht1`K&o=2FhT#wzX0`45+ll$tI_J41==L^#H*wg!UR0}TMdqa+M z?l*HQGBXtn<2i=!-wt2Sd);SmMb}SEBw2BexFVLaH zPphP(;+JE;7JHVTyf$gSj)I)1iieI;dN!LXS%KGyPe>hc7FGmJlDA{%L@$K6P};zs z^$G@w4sO}RFB!j59^u?cV?t7%p%g}f(zF;1I>|{rOO?0%mOibgdU1Ola*C%Z@q_c6VWBgO z7KTsV@3`^uT`zQcpYKTWBUICxn#p=MH{xw9W-h(%KGVVy{Kq+N4a6L%?+sE1NfDji zwKzdegt~q9m)-jlAa1f|0bcxOTS2+x|LMRxm@v2>*{z$kT=Fv;I*Nh#+bjzK=01Mv z7XQt0x!c~4{b2ts2lnl8OC1j)OL^l&LUr6xhcu<05P!<&tPfxIxN)x?bw!hDOd z{?A*yk8|G4GUqjL_(!zg9OH}@_PIZ-cK+H(qB@T&er*N&lh;^a?`_&(CPy}s#`=4=`;Bb>#jta)sE_p04DyZ67Z`@L&Z)upng^+j*&v>t8SWcPi==a6uZ)2~W%JrVm2{l?jf#Mj^{7NZ8!ZF5dZ(e`}J3SC$+jfPViISy0upKx;b^vpr<=og%lrxbDwF?i-KMtj-}ToM|NeJ=|Nq8~yz9UGUfus)RTqDL zt<6PKx%d5hCgvSUiFmWPS@PSLe>eO0f3`kR8@Q17zGwW)gYx%srPL%(J$3l>K=!@! z9%j{Fk6vr%Km9xNP-Dsr`Fq=Ct_VAEC_d4jm11+?;<>jK1rM~}Ka3Pu_j|$HBlpGP zF6|Ww(`EiS^O2?bk-d|q$?ZGXXghsV?Yu79O?PI0Z#-G@KF+Q_t6 zbX6?XNIs|jsnDQB^3$YG_Df0yF3W!U7iv32{fOYFcdL!;BjYsv%l1!k^|6nO_l!@S zaM{KqRqIK0z_at=tDku8jdxPdW(MXZ&r+@MP;**0=6BVa{lYSEw}ESs)?T2we9D= zT~qJ)w{4qqh}BXy1X$z!2Hv>Qle%9cf69E3=dT}ra_gQ;@5>LZzj~7YWX@i`d(+o%dUx++|DNdepKObw z`|JPSeDA$ENBX^P|C(t>{|7`qwf&wtwc`H%?RQdtmwob+*6;h*eyjh)|5*7+O8QS1 zzdpZjz2E1p@3VKi%2zM1{qOMS{qFz!_H6!tn*HK`PN0Lqt{x>u0|JzKN7Df!4D7}J 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 1aead0fd..f4e0c2d7 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 7e07eaa9..b2566e26 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 d4c0a86f..ca354f29 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 a3532d49..83928c3f 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 8c8679e9..52141364 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 4f2795d7..60e81604 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 d4fcd16e..ddc0e772 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 6593f180..1ec963f2 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 5087538c..ee19ac48 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 ef57f375..15a1a23a 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 abec5b23..1e3bde9e 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 1e3bde9e..20334ed1 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 6000552c..1f4fcf2e 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 6254e988..9e6ea1b5 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 00000000..c5a9b529 --- /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 176b1ba2..7703182c 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 ebaf8c31..403951ea 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 3fb3f262..48f6c109 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 b107bb91..da354579 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 4e98f311..bcdad4a2 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 b33a6dba..4cb547aa 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 540cd9ab..108f5a5f 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 a5535c8b..c2a5f1c0 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 ee19ac48..7914a4c0 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 4cb547aa..151ff121 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 c2a5f1c0..0189ad9e 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 bcdad4a2..a2c67551 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 151ff121..896afd00 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 ea2dbfed..9f1f2320 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 bff0cfaf..fe0d3107 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 12d69804..b7965f23 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 e029f1cc..699aa9bf 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 a969ccf8..e7668a94 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 20334ed1..8bea9910 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 fec8b867..ea54e113 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 e57994f3..9e004376 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 60290699..0b66107f 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 e8241630..716025dc 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 6f62163d..a034b4e1 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 a1322fc1..a7ac4eb3 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 38121a4e..bb0d071a 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 190834eb..a77b92e0 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 cd5bd8cf..c8814a3b 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 ea3553c8..d620bcbc 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 a2c67551..4e98f311 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 896afd00..b33a6dba 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 7914a4c0..ee19ac48 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 108f5a5f..540cd9ab 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 0189ad9e..a5535c8b 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 3feaef51..c9af6328 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 693ae7d4..5c4af56e 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 992fff85..bcfafe9c 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 de1abd23..054a3958 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 bcfafe9c..c30c6c84 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 4e98f311..a2c67551 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 b33a6dba..896afd00 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 ee19ac48..7914a4c0 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 540cd9ab..108f5a5f 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 a5535c8b..0189ad9e 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 7914a4c0..224052bf 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 22700264..62289d5f 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 c8814a3b..d3f7ac14 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 e9b8edf3..a6df6dbe 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 72f55b44..3c8df31b 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 14e8a463..537313e9 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 e2935770..d5a65cdb 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 937990ea..eda3b8a5 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 a567877a..774cac7e 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 839c69bb..872cb30e 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 2b109c18..b7cb5280 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 da354579..52ccf378 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 9f1f2320..2feb026f 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 6ec1152c..8398fb72 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 295104a6..96eb6365 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 5604a0c5..b966b7af 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 7b979aff..c144d786 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 a280940c..0466c48a 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 1cdf529c..c0d0bec8 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 59d26135..fd8b520f 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 ee4b4533..8853afe5 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 66373a5e..4a391c3f 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 5a137f39..b4a46c59 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 c5a9b529..148723a5 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 8103ae3a..8e4411cb 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 c735cb28..582cb58a 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 6a17cc90..cc35e3f3 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 ebb43f6b..3f11d72e 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 d9463318..96ffa4d8 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 146e60f9..2069826e 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 72993297..26bc8ef5 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 a78509e5..726bd8a9 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 483ee67e..c7eb2720 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 459c517f..e6d2dcb6 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 ca99be12..9276b184 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 53de4e67..6649fbaa 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 d3f7ac14..c259d54c 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 8b99f613..4a2f6164 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 0651fd4c..748cb39e 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 7703182c..56dca4cd 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 c259d54c..c7aa7fe0 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 599dfbd5..d041a433 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 d620bcbc..5d451e4d 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 f4e0c2d7..6ee57ef5 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 2f10ac00..05fc1fc5 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 62289d5f..b9d79374 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 d7340b27..322c324c 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 27238db6..4717143f 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 cdf57ae3..2bf287fe 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 c30c6c84..ebaa160f 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 b0eebee5..6694d8c5 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 81797962..d9684bac 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 fb67154b..2e466815 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 0a10d801..c4b07b37 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 15e98e52..11ce96f6 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 c1c84f98..d1a8d9f8 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 1de2f89d..1df96f51 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 e625fca7..c807479f 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 d1a8d9f8..fefab9b7 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 2bf287fe..c2d5a81f 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 68b9c37c..96fc2fff 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 1df96f51..af1bc4d5 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 c807479f..9d62b87d 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 96fc2fff..eada6971 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 eada6971..817b8adb 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 af1bc4d5..45aafe7c 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 9d62b87d..86afd5d8 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 817b8adb..954f1ed1 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 152f502a..4a0a5340 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 a77b92e0..b778283a 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 c7aa7fe0..e4b667d5 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 48f6c109..4fc5db74 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 b9d79374..07944649 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 3b1bcfaf..15ef1b84 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 ddc0e772..598b2cc4 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 f953c5c0..467d918b 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 34276426..ac3b32a4 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 b8473072..637fc66b 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 2d77b1ee..e718e438 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 zcmcIoc|4R~)W6TnFvi%&7GZ=zhO}6cWk}(Nnv%&VS+XR1ma@cS%bNTWq0%U!no2X- z$}%ZSmT0jQAuY%fLSud2`MrO?|Gww*d_K>;_ndprx#ygF@Auv`>LDv(K{-JHfUu3V z?75o~047NVJooW75w)FXM#NOToqk%uTJAtI58Oe|tmfMhlzZC{b;C4@(le~`jf zgq((`T|&lwBkP;U++U<&5OFO<{2wE|6UfpkGBl0gB9Id$i2Y6E)iAtLsR7EfP|JnB*WmKad~uNar_XM<#OQHu5;{eIL&`%3((j zIRNYyMH}Cc>X%}>T=EzL-xiTd$D*#a8Lili^uwR8lJ1N~8)pC@U1Vcve(VA?&8Za! zL;;{{*0Z4=fr--DauNE^(f=2*Ur+eX5dQruRkLOOQ>Cb-y}7Ia=j$hp?Vd9~cKkK_ zq3HflxgoAWt5ZWvBV%me5@)3+;-Z~Uqh>SruJG`?jRFtZvc`19_j_o=Lq-=}4MM!R z(Y6jFRp`J8=}U1{VTpIw=mz_CugkE%^u1_qw7P_7%ud}JGZRDNWUjn8#tBG8Btk;N z(61c*%sL~8GwJ#}Pm;om_Us;i9elsl*ZR>|fviqm5ufG~O9(ZcaZJ!812)aJaCcX( zIF@xSB*0_u9IX%h_2xd~C&ujoD|ewbCE+uB@n>w3vkmdq&^Xdvg=wGd4b|^y)L=KA zJLraQZ#UknbwRFu((a~INzShAq_;w)C<1J!D=aJFgY{+D-SnbzjM`7%*dm#4(C%?r zX#4BjbB40l4Cm0C9h9NBjFv1V5DPWw^B>Uf1iE-Jy-uO@PITHXg~aW*lxPt8gbkwC!Uqy0RGdFXwYNRDneudaSouJ*?1{#ivf=fYEV8X7 zXk+=q4Z_52fRl)ILjjF{?e6XVG~_Uhx`>~)czW#6@$}xp{i`av&;*L}*r4!{K0+U_ z#rO30_$|9^=9tIj$G%bb7Z%S4Vd&BCR<-j+1_-Ne2)Lc$Z7dnF; zsEiDK_cM-dr%t(TVKVb)WJ4w4XPvj%K{N?)e${_3wQV3B$6iF$lU=K0x}@;50eM{r z(3C5kU&M5@Kwpw(vVua_9(wHP6wU{f!HbW7FQ^Zt+Z%lHZTPAr+*u{r&Tj``2m34N zO^Y(Y=g=FGI8E6yc@a01z{vH+8u>W`rtxzKyD1$PLLShClCW-Sw%_2!d$%wy<1^eB zeLo&x^MM64CwD_6?u1~N)LDnk2J;9S)($j^SWmOjoADAZDIefCk6PIPh`>!Epe)zI(Rjy-Gfa6&@X0=LX3~g-IP;4}Fp==Z4}c%q_oCLk4QIP8dMjS{id; z9H8m#c85O5O+46@u~%sd#*H>mR>t-5wPEJ{16tDGtGq?*z~7{(d)*2Akv)2SS;^YF z9py>zzEkTb$o_Obmv7om7W-TPPYAS+$4JX`nja_5w;VlX#p}1K9CDJK=@w@KuNM5k ze993fz^53V`fi%D9ZR6d5`>D5}?H`zm^3iGJTjh6JSC z?is~g0e#HAS{@4=3T^r;z|i+<*4d$N2d+{_YGQ6Q4RU6*9_nAqXNXCs0&TY1QE}1myJlS`~F$Fhz zgDTI`4-^lRk~5uJoaSKWw99>*Zogf71O#RJG&ShDPz?H}Pv4pGAwgc`ob9u!aHI0f z{T#W=6oY|o=J9Az0;fOS)NjvlLLg_zr@mA@A2h%%$$HME6oW{uK4xzsZq#9-h+gI+ z8b%NdVt(4r%Sm-=_ri|hOqE8r`k|{{C~i`&_htUBCk=4}rC4Szf+BDNnsI&vp>gR6 ze1)7=J&u{93k|^^3d85m@4)pKa2^J(GT&8*5IBwlI!;&Ktf-e6b9A<*1oV*t3czDl zJM)#>yPN&EMKqklB=a*<%y?D^cE->GBLis)D9g7B#tP1+Q@XQFm^q~$1@_rny$y!| z4$3}L;_5;R`oZ%<&?wVM`~1oF6Tk+P(}skdD&0I$T+vq8QPBCWwt1Jk=M539s<$fb zkN~BjTaYi~4oiCo(i%C*cHV*|fdCm$?Pv!|M9QhoQ*{FQ;HWLloB0)BsnwD0$)zs& zU{mDFaoRnHUr>qS^m>a=xPB3C^mm=G7OQh&@Tq)X%mi!urXmS$Y)a`qYv&+ht@QDV zAMKu_l_XjWUQNh#`Z)}}$1N6}U~Q91_XMb-q1I1#We%NfKg%3(M{!l^4!>gCR&$G% zP(|7VjuO3_$tj^@zhEU&pocw=6YM~F75!A>nfO`0y)PaO%8bL75NI|p$H`Q=luw*Vnio?ILUf8s&PV&T_g@n&&qe0HWDTj6y9qM;LG zTvo&o_fv69lk=UDOstvg{lXY8#B^#bF(_m;c)e8f0WLoP_hEu-Je_??yk zR|BN`pV=Iftv2wxJthNkorXN{f-)c9lD~?Tf$u5s48MlI?u|Cd8Db#Rd7VrSaG0Q+ zz5Dg0W+2qoi7N7A%$YAVjfw2UgUaL5Qj+Gy$F91{O5O&lv;*@vL#1iSq50p;YGf@CkI6Ccr3c*rK9Nl@kbfk)A)N5JyISjh2pEGpq* zCW^D1%ja^HG1o-E+d5>T#9W0|{T(#kjC|3NNVNyoXmcM`&xS&`{tjSZ%M89pWI;XE zedM-_X@^16;RN+iYK@Iu@HI_+DAV(Cj_m91{$PMLA(`+=3T17srbwLs^*bRw{9g>zcI zVS1d>4=N^tI%Rx8A4SySLjJ36$rX?^4xLvt5h`o;V4Ye-w^iRY7A4pI+R%<~!_J9U zWJ^xCGnzbBD<`mXl3%S}zMpFPywZlHc+DU9Ac`)FpZN2*4Lj;ZZwtfUX&d3MVV7fJ zOWRnJ;H2Yx3MxvR2!*9Iw}O$qItNVQm=hP$RYm^Bd!KRxuh1*^o@v$snrV||?x=qB! zHKsy-2Am_mwl|@H%aqBP$B|n*b6W-7l=7Fr+3KT#rZk{A-({hqNJ(q{?jJ=gub>mk zmk)c7zeAfBHj|ChF94M#*UK6TTXQOTv%dt%Uio*}OJ&yh>%INdiDStzeKYdJ{-;xI==Y)s}k5;1`rSqI~!P0~`NZJ|ptWK~qpd?-BUdRip{u>Myya&_SiyT`^TD zOK}Bw>ty(#?pYI4&`S3Tw6 zsMJ>yenDQ>2Wz%IsBHbriQ4FMNHlJJKbopg!6+tV0NpVSS9GvjT3VJ9hPp)mC@$WB zsyXml1)kCBK&&rG$r0uwvo!dLx-uFKp4&0gyE2UWPfGw1GSlCoyx73&$le=wBwA{W zn=87;(_w-*;uDhSP=_K$zmhOgr?zoAo+c#|*r39dmBd5~%1B-(pBEKlgDRgqv8|dX z=S;nDGoC{z;#pQ^xT55?U>aFv>c|j^YP?Y0`Y|N;7a>OU3*`tA2-{M6(g_X9$_+26QxD;2PqsI##sY4Tkng#6&aXlNbToALV=28t7cDUi#78cUofh0QyTQO>| zl!(Gy*|-753(9$^3W--3JD^d=l1MzNP@=^Al;1mNMj?TwcV2pz9OX-7dcJU()^G31 zVu-r(s!WH^s1os;@2)vcj*H&|uZ!P#I}Uq`Sj#wF_UBl<4uW_~fbL84E(mZUDo|Fs z;9g$pKH|wmc!%@X=3tKse+)R~RQz%)4^nA$l9<%!-eAzI6zALZ&pqwceFV7L{oeYt zzb#P@Xcf$|oyX30*j1bS=S@7}y_(uSEktow&v8KLi(?GGxRw|-4R z?7s1FFcexrZ|GWqM}MLl2Cn}yzREQB!xK1-O;ycCHP@$i{stDeH~$#ADSQsi#*Mbf zSILD%9Ojef&X+ZMnhYh{lVJJZ1AgyEI!$8Y2KqG``I+`49&WwtqQ5x5TWQ1=EL|_| zmaA__v;zjG*!{k&e|Oz@7FTm8MMclMKj+EtzxRS z^!f7uW-z{Pk6+aa6Bx?#Kr*@j1X`GM6zXX|$K8jQoNrS|@L|SUOO)_WuE*bhF7xl^ zndwlzOn-7Kafisp2k#Zn9NZ{Azml%im_GRXzV8ox9`Z3VIIn?d{>W>HIC#N zF6ne1uN6`EfmmV2HYWk!KRX{Th20+H^raosg@|S!#o*aAqQQqm97WjQ0Ma^Et4L9C z3+KOwX~QrCZkAPTf^Z{@F67>{0`VKLr9>YgJKl~ zlJQ#erKd;E5kUL{{G;Fqx%`2NLF3{($3F`{Aq&2`6Z z_9~>>0XdKyc4FfWl@&_vbeT30D*&?*3T@m6BY5I=cKN&pw2i>o;H=2<-?>dw2y@J7 zwiDHZ?ZA0H)l}IX+eOrb+@7^89cZm?b zFKO#a%FuiP;xK?>e)u=PvbwWqq(}To%JKzfH93s2SdLkR8C>t|Y>JBqg zVA2Q06`XGR&@}%0tQw`ixM@T(^M;KmaTk0f5^YR&yt&^bsf7qKG1I#P2eDF zJEAaLb+eTl{v9kFn0UD!udvo0wKb_2#NZ-4hA6FH?yrq}UmTwlUx=Hn#!wIQg>g0b zk1Y0t{7M%3b(s1dx89HqZ8>`*Ws}>?XH@S!f7)mh?|s_IAoAl4l!#fS=?p zmB-X-e8E#R86t!B#})@qLU}+Mgb$MWd+NSohfx>MR6W%aCl5QSg6}ia zAro5p$goH^EThNl#B=4*{F4_OUehO@bj_c8tRT|4;(qfzd9n59p38~n4I3(?qx)M` z7jGP$eo^m)HqFl2(azecwVM2t+|hJ<&V27GUrZoY#QMLtWz0t0mn+o&O8*y)YLSL- 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 b1cf151e..69369904 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 225ec34c..bff0cfaf 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 00000000..c001c58f --- /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 dfd76405..b107bb91 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 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%0DUB){U4Ess|GV&(7(tHoy(1AJxTUO5(rOw zC2O(I>kCxxo*|Zu(0l*oFec({Bv0GWVgwy<8sIgGcI?#6&P>Q!{%lh+g|adjjvHSt z5h0a(|E%g$;V__X$s9|!Rhh7fXh12UHZps-JweKYdKZMQj`*GT{XMG4kLf(Lrv&Flp2;B_@JC zC%VrWJ6p&hFyWQxpsuwEH@o;X|j|egQx4-gP#QFuUJhz{NFp0Y`@Tpzd z@E$DP{joP(Qk}87CJKFw->ScA2*MJk=)yCMy-(`D<85#a^F#b7_N6G??kqgy`>dHJ6T{ zCzLgp(Jc2U5qYx_rM8jQk8}Hk9(~d3&s?`-N#d4Cqe@@;zbyY0S<-HGKi&IgTrkm$ z&GXcB30Z3JUYGOA>Yvu@Fycjw2pCd0wak{$s*34L=KLG2hUdbJNvP?=``WlI#{RdT zVyRP^wpyT=T~;tK~L1W%nDjb!mU>o5OgPaB&sMs_4=p z%S8Ekc@93=v6G8WvEM@5X1y=Od%bGhZW(R|ho?Nl(S2dVK8>Wb#dYEuRe?T+VisM3h+QHYgC{w1vjYZA&Q zCHfjF=)iKhLG+=j;7pBaf)RU4%1`3j(x_WI_(;V8J^G6N(1;UTl;VoqKnxp&Elu&1 zMkGA))_xI$Xtx z46_9RE5HStDo}8t7~ytC5Gk8VfcR{yuI6g;RA7PRYOIer#pz?%Mo5dNbSD^j+r8s> z0_CsU=T(jr4j1}yP|iag-%b~PcPdUmeECK#6Zrs}_c%&tsI#{CFe1!)0@=08>16|q z$iU(3ZNN2(`PhJDO-|Ilz{XX;R^+|==epW%qoH!pm~6o7iqYYWx1nW)<;6L(KU80z zTzjz@f-CvNc;`JOKF3YzEmCo!d?Ju@eoiYkN+NQibL<0YEVl-~dCl}Ra$k9gDzHY@ zggz-IQR?57Nu$A(U!O{q$J~`DG31W>`B3WnW=pc%r6J2T;e0N0QTrJ;g;Z%?%v(t_ z`7~|e+PCg&fp1Tmz7%qJ=6WUCZO#_Uy)&QDDn|*cPL4=V;H*T;#CoT!L9m{$`$t6K zD|@+DVVYC0(aaQ!=4G(8zL&aYyykW+v+wb}LrWG3kMY zRBI7FGyK_#*WB)i$(*3$#e}0&9zxH)lf+ZMk6Lp(>OUct8N4HJlKW0HR0#0_Vz05}nkDxDBE^UAe8SMI0Gr?YMcZ-BDj!|LxkBs1h}in2eDnU&r1Qf874sZB7x{0zc86zQO~fLoM; zt$LI_7TY^wBTYrRuLv-BW%|_(=;+3V;|d*o6}53S1~r-&j73xa$n{AO3yN>98wK*C ztWzK5Gu@{P@*!=@O#3yE1P z6xa|1yDK$6?dP-^QFh$7b{0_43Vvy!D(7a{Y z9-H~W-&lC_Op%((C#-23F#r={wvO@zN$UGGHs%5T{wq|9Hh-Eo9pmXq7&_e6 zVy^raG7gwOTru}ho%gy~ZzL4R-|9GGgdq){Cs@VJ6(zJNZvdiB-ZlKgD$ew^M8qa8u3J8C&+KDDPl{7*v6 zCEDK%7Q#2j$dJi!WVmd!KJ|cr8he%askxI>Kf~oL1*$pT7)DjDp#EYtj>nZ2PZROJ zvZ|_fBctrE{~pM~H~U^*LytbbAOPPj*3MI^Pr>X4vrXkTvl6P9Z=8a@;#e?77MhIS zPF^OXbXLHbWjo7MHs1VwUz<8iNH3eeOWvAY`B&k1KXN(mr5k%gz*c>My~bR%-DiMi zTX>46{PNhb=8{l=#^)Wolerqr;T;VY)wXZQIY&Mf!ePk$t_Fk-r_$Gj1{4`bz?|Ip zI|tn>t$`m|7JKv-7R}QRCw};RZ+R9n=<9Fz-8*j8uQnJ}tS#mp!?A5ENYehQWvLiM zp&sAzSGF8kuy1_T#mU^$2OCa2dmC);kyc#4^LOj%5%4;m+vp^esJQ;cf6o(YQxGs3 zR3SMs_I+ldjiK?ENS`EAk!n`urs+3?3NTsKlquBNt?e)};||`-XY~vpTZS9gA4cgK z-%ywDtFP)w-)T-MHu4OSE>=;4P1A=Kdwh56-Yxi~Uz#78m42l7&^|NsI2yF{#n)V| zJL{J>hsr3H;CXLwY|SOV8VQ=If-y_{B8i1?nVN(Orxrsf(g%>bi9>+DUv;K1XECm_ zmL$M{Ozizlop{7+%xZe5kK8EfAUV-Nq~}x6zFAgMES@Y$TtRx3$q3N+2Jr@8_z5TfQ8@j(gLVV8!J2_O%=0GfO^upQeYC5Lo-I9@Ki72LTix5#tCr5f+br?6mN)1M%kYe{v#i#SOh_ z6DGZq!9Xi%r?(U% zNLCsmvJ`zqO@O!EH8RBCU?Ms@aWk7I%IgW&Cl0t*vMR7J9@Ru=sVbPDFZ(<1`X$oZ zZRqzeHCZyfh z+))zgH6Sg)z&&?x8)SSF{6_iWhG*psuY8E08O_UX>7I&YRy60`>w<%i_}>DbU`}?O zlSAtpCME0H#oWjH0ks;1r^$r*a(8L8og=I)xmthXNZEQ1WAVBfUC}KX!s!v|9k2)1 zgKBZjvHo(R)!t;zV(tBn{<3jJ?lEBzevq^u%MJ6n9OR4$-d8}4yHY(6{PrmGnG1E< z#b1c zh&0-=q=?A7-Q6vMZvNH%Cv-UH=tkD!^CPDa`#pS%Qpnry;O?GkV;kY_&Ia(&ZJcl3 zAN*3QMEqN_B+vH9w7u#^^K>;DU!{U4A@C_ZWS#G&P(+>EYlJ2w^fTsa+Py+@fWmyt3F_a@~3t(t^Bk?b8v zN6Wf(V95{=2sv_yCkGme$s=)AR5E;qz349zVtn7Lm$@u6zh3drNV5Oyt(X^-K<-G1 zFlU;Cr~@W46H@~4d3pZKDfg&mmPtITPP7cXMuNIE}5~sFm^kmFl;(UCnlY%XA%7*M1#B|IhfbZ4}Y9+n4z0Y|v%E9;; z{?5HJtX^jPFs;lI@2dowYHqpRmG(2oYfGJ2SFAv>z-Oos_08gHI`hCD|%S9bXkE@6zWrg}bnvUhy$gCLP-<@;<-aXstER zET0ki9*aj(Sg!DVJy_fnTa)P%8OzNIqY)q7FJ zf)6SfK<5`Y0(Wjb_cJFQO*z_22-)fL|#UR1)(!7k9)_meJ z7Z-EV%wuXN?cxU>9%?MbWq8T<^c_6o58Ngxt*@CsdwY0NhMO$4ymH>XEKh1|kXuLw z8|ODH?-d{8_U7f)pT=9(TYOo~*p?7eF>Wp<1+i%M<^YVD(@Ih4E!?jq;)L#*{Raxc zA4$w@asq2+V`mob?1GJorVycpxzjB8hyG_6il|-nH-}LpYzds3<=Cp z&fsoN$kcsA@VhM6Q`z%Zqm-RopgYc;>?#kaPw*T%T}00D#{uNJmHpH4&BqN#56-=r zIo0tWa-Gma3I4^OP84gy_wDuXB%K}}odR~%ye#3*7sMhT`KasBDQ!nDzC7;zla%1H z9{^m|S1ow^3^N^1;2V7pY^r922O4spr@^jqiifRhGk6yYRp-=VtP^3?w?+|#>KG&B z@Sz@4#rZGu5V>Yo*DB`2LyoJp z3Nw|(k(8X1skMAfch8h>;$?wfL)oh=dV5yBRXGIB^`6sSy~o?&^K#__)&zVA;mq${ zpz^)-i{w>F)Io!)Bm00JkIn6x+EucL08&ZMrlBYbSoG-a9H^NBGJ$We*6?U5g{{Sf z%}jIScq}$OpXBM;yT>#qXg^Ul(c^#6M>z`Qw3yUJP6Q%Kt(Q03DV zejp~5X%UP)TVp!)mGmd!#G&{1;FJ}MQRjvI-HDhU;udzBQ7B&|lec)%eV)wjh@Tvt zFSI{4w<=Rl9g;-5O9$1|#zR$r?Tqe>h;gK?pmeHQ#n7WS!3b9h z$eHKlSc2qAiWaMss{Ci6f!6WT88>ldMO=m1;!7ws!23R$J{#d{$bVb384tC{ zi*0L%zk=8pna8*(s$L!EhBnY&WWAT2ysITG&l(Ncn?%zdNc(V8;ex?ebhxk|feU0+ z5G4~wpLSvmxD}riQef7YCJ%j-lR!}wDLiXG9S=XpEFN4=4)qNzI|egIPs zjJ+4^x@GOmR(Q?nf+j!e%tZuxuzkoioKa~qJIx!#b0rC5#i$q!R4At-d=tD|%lgFT z;09zL_g!K#X!^5VD`=FQ@okx(wc*0`KEIQFVnh&XxH;`>)F~Ibzwe2vZij{?%Aaug zcu?S*Ugleu-#pYh^YSCW`paDSOV{{CQR5oq%p?wjcCNDbWBSVst7E1?>skoKlVA)nd!xX(YIiFcbd()} zj#paz;}pq+x-V~kD^NB~l)B)8dVtGDo+1^;k)|l1iuyK~E%J(~v#O<=2>-Q;E5) zximeZiG0*`FpMn}QO#`_@o0V*b*w-ZDx+>hcc(6F_i@?%=zV!Mu^0Ax>BJF+lp&lh zdG(?h`CbGf>))yYN2{Rko>>s_WAI7)m5AnhC8tlLy$h&)*LBLcn%iSVZiL5C>h5Yw zATqJ3s>;J9X6oq&8;t`m3?p9U7$Vbz9G*BzKc0T*HY#AOKbDe5I2xI#heoFn7 zt>I;9HR(ehNvr0gg$yanv0+=Gq z{hRLC`R?PQLv-q>=B@T~*&k&?gWK^rbA#lM`oMf4g)VaCqP@)WrQXz36jVqmxQP1n^Ou%n=rsOlE$8>9@(BF;Jlh?tu7Jz{ z!Y2pI46bO)&sc5@q?{WsU5_voRsQ2Rg|(Jio{EFIKtJZm%eTJuRa4K&w~}o`&i$M} z-bhXO_{3E0Pm8~G?cD8(OmSo{> z?J~_}u6YCP#*_!wd8t2-A1&V`>rFie$Hu2X2O>;Kc< zS9aAAG-2YBAPMg7t{1luTm!*9cyM=a2o~JkJvdz4T`v|0?(S|EF0$l(_w4RJ*blp3 zYR>d@_nGO_)l*$hbv>85sCBgd8dRNaZ}=Zu_v!VrCj`8GHE@CIc49;k_#y)hEo54q z&y>Fq$IG2@eVIj^`ln%e|F!nCoXlsw+j+qUIXoVGWELcE>+|zI%)+n1T%U|N9P;s* zeBQj41bXaOg;xHeFHZ}!ceRw2mrie4W!q+Bv@0rA{LM2z*0QA8U!fvzn`o%tv?WKXYw73fHa1ueDTpnm?m6qDs0W znN9KkY7f>OEL2)}Bm)IV=&{Ta(N=na796VkoHo*~L~Giv${lBEtw_05cIfY>$nYw6 zSdXO=(l4K!NZFUm(R9qw9=;hye8;&|7LLlJ`;yo zx<)b?hqO*Czq}?azfSwV#7vUu4N+b{@Cmx%s~Uf?2~NPFnhcDS1Bwrr|nwG)iUi0 zr$YJFK6U4X)&Bk{DjOtc>zyeAJXbG1_p->|ZHce0MoP-$n$NO5V*et!?%ya5-jHB& z{kh8@!5Bgzvroq|zAIwB2bf|VT6c1F-tGKX7XqZ&z)QQwV>|w*tq8~axnu=P#y{1L z-pPJ^hWyRl3&H$}gaHn8aAVFWq@)-4mT@945#5s}7woi%tHUh_2eLW4JB#$3H3_tM zoEX3QXhFomY4E+@RCUD|8I`?4m7Ov9Dbaq#h^W=6%ZgqNg}LD2gk_8A^MB+>c?Ac+ zqKBf^s3B00y>rY*fXB%9+LI$RH-$T9ECamb`A|Le8BW_M1r6KrsGLPnuKE`_4W-ks zBPW^xg*EJ;qmj`>b=3_)%Cl)bjZ3P^_{6h(G~}#8+eDMwzXXy2xN2ZKijjooBoeng zjfTuA4t0}lWF(aiEhR0H&&5lBp$9HZ^q)or_coeAfm^t#RVs$ZOqP`3pD^65hH!lR ziGk+>;RQ_;Mn0Mw0L$Z;fX^)sA7P4Bn)Wk1*fa8IIq(Aq+4$ke*-*oDmlGRU2S7&F zJRnD7FL|jJb2)%$=46k{<^BW1Eo}x0^1dsUw(8&oA9DtDKjI5fEiShFKce|iD~V8 zlNU0*Q1=^N-rZ&d{(@H#&vlxAPMp844&m#1@uSYmR(+$jUiW4lX7E(%4g)0H!tLC$ z!X(ZGLWEipj&}dzCK;zw(?h%7e8Xt^a@DnpF#6fl1^g$h&_35EMa?N;`Zh4hP z=ttJL{$|>Mlx0YP7Whg&+THlyaU~|Wi7rN8s-bD~O?dOzo^yAvIae<-2Bug|6Vl(8 z2+$hUI(`n$IN+bia#gMCDS(GpKzTq218YMS1I+!-y$8d;>r2tk9ZMP{&;DnKd^;f27 z{9`b{1$|I05U(~>m-M;A^bRJ=e^pVTI1@G*byTkTM~Fpuo_Qh)tXF?JwzNr9s#F;pbreve4l2$%Qc zQ|kLKxiDA<(jN2FE)yQ0GF16QzjKyBfRm`+$ywRxkQdEWW&(X()mUY|ouOe5Wup|k z(wKQ#nms)ibvsk_m~DhEXj;r9qaf{b@u730I#nj3nI*@KQ=)Otpg4ReB87$^SC|>5 z86cE8>NbkN;u~8UjMRb~(=t*JiEWsz@iW~?O|+@bP@>Wh=W-O{1IHqMkr~~$5{~eJ zSveTn{UBpFJyS$IBmMyjX{c|(@1vCcMFOS;25FXjhQI*wAx5zfbK(dVB%{P&bapJB zcrMm*S&#`hN|n0YZuZ)xz|Y@6Exqr33%EuJ_A3IsFlKp~HY~O! zB|)~M=)w;S41JW6VYzSd4dOku@7(Hom?zCa#3EN3i%XYSY<*dMt?q=*%jQar_8Gs* zw)<^x`@*R;jm&2`DyZTszHWID6ocdmz(@L0CG@R({OYpiRaux$^+DOAFjZoV{3=Bi zJUTk_;f00*5tJ8*<(F}pO?$n9rW{-5NodveXhN2?%5=)I2Xz&SdQQQL@RMoZcfd_zcUA#V8|>jE(ug+TDPNvHLKyNA>ivfs$va@vc~g^&$7Crr}>W$ z_Tfu%b&m0$h;PH&ubnrF`I`c&dAowg<8PhtvnwC*ABi-r79D{zZ(Z#kwcRZ5 zi{^iDeqSEb*^2BpT3x9Xnl(q0;nVvp+pMGcs>zksr8O1)o*-C#fU$yMi6UWMXmhv4 zaWX7|zzk?7#mirz?5b;3*P|0)%VelwJ;}f~-vEkd=W?x)*G0s+kj8!&Z9afWxb0Qs zQZ+4hEct=H@YZ<9hY;{RV?(xWV=8@PIEAjglA_RZ2uI(B^!2 z1$$}tVGt}xmAhQ}9oRc28&;4Y_WIz&4cj?9E`+H08qk_sFe<3e_ z_V+mEO`?TfO*2swWKsC8=6yL^gJgVB^CJt8U3cr(1J-Aj2?K6>h{fW?AYGA8%?s1M zoM-%lbsR-e&kzbtONbszI!i?wFs;M&udUB_MPWCg=uT>&w8+adxA)8L>s5X8#&3@; z4^2SF0!v@X3Uml#pF(5}z4|DjPQJVzLyZ9k$Y}*5o(G92$VNFng}bgpaGGXS$raL2 z-S~22RilT7Bsp{bx^hyYa`5sTeYpCH`SLKM?gMxntiUhdJGf$2ICmR6wJ1o_d!2-;#zG-LL+&UMTB5EEPx3_cPsFH!fsLA4{@C(=`jnE6mZ<_XmQiZ z#cTs;?TjfoWzy-wcnm2=I76#8e!3qL!uoC{83A_TC45`8emJ$5pF~H4f;T?O4$$e? zna@t8g$Q5sNrQ;9Tb~S#FVKTj;6#U`|4wI-VIRuXM?Ui5x|?igA!y!wV6%ot&lu{U zLmOKcr(ynS81`Fjg1%F87@LJD_T=Ex=S^;KJ7)D`m6qopP0s<-;lKTU7r(qH%EcfC zdyU-^N((J#RPX!Z251AY@=E+?G*%s`c|saL^)^>93?A?`>1m=iU;1Ky*Vcy1xfng# zu=nBwDJ>8hRg%n=7bH|WuP-_-Na=);?!`&w z2=p-aj@sy-%ig`=QhXIC5)|R(xLOCaUCeD6Xo$U-A#vXNS}576!Fe!5crD9ano;#cF$=RGp^h%;5;{MNc{`m7hB2C zDpp*-ClxeR$K3Eh-Ui2Cah4KT0yko*EDd|ZHibXfqaK1mKXsyr13#O^C#n5`W$=gJ z4$orv5CY%;G^+>05MQ%>`<0=nivaEqBDwx`%PToVpyhrStf;hbj`R*FQcP(g+V}91^sB{5 zBK?ZRV5`NptmeMck$?7(s2%SUOJC_ru!FQxI_M{l(>rqtqW z*6@%0bF!x<`d)WJB^@^tu+k#^@k^g%&#NQTd+a;*s-wF;U24Ig!@Oy_TeGOd<0I|} zh$iX>4BhOrpJKfqS4>Fq+5NL|1Y|AS;QTc+nt_}6&lugI!Ma@(tHFoyRs}qtOU=6X z*S-J%bN#21#Uj$8ITk?jN%lNiFm5EDd>gg5JEMtO3rGD0}n*~~O)h5y;4i5~| za6l8jE6KStcqi1rZ%1&wbI|LR_!fqM8_$DPz<@x~9&Cb(5zj{3{bK=+Bp~Fy4!U-l zEx%DU0#rXz8L$?<0?2PhvvCoakVY|AG@qh7LECW-7tZ)r02`@8BYm29ED3`khwH>xT7y2rF{=~uwLh;CN#mC2FRbP73-pCQ+UP&~c$qi1 zWd~W&9)y2~+Flk$LMcwSbjt3BD)i-fC!J0@rM-0OE8H4z^t zPm+^ZS?=~uZg>_p@V(^3`g#mEL>>b~H@cbXDW2WA&)7Zj9$BLOW#3u8@HEe&-gSrP zg#AwVxb&&DV=BzzBK#yIkE$KZbi>fq=!!HLb+UgyX`z0EQ1f(VYc~R84YLRjdW2vyw>Q*lGU1tp*a);mMad^qI%2I6QAE@V zxFc%_>R&2&rpbCc%Tf)!v%+;X$+GY(l)o2Au?f0cJbu^O{fsT+1vpPAH7Cbl+~#tc z`cileri2LVbK_?+xknO%qDtdQ;nE$?zTn;B&o-A%_XXa#K6NE*=j_)axd^ax5&B^1 zvsrLa4!ckV#_t!#wtUOaBoap7RsX2Y%eGNJ6fY*>MZhif1u1mJ632oIY%i>$7)UB%zmhdRVV+rHXOG=S%QioUyWtb# z<8Jwq!Wm18x&~{NPT~6wp;AAGUHDhDMYwwV@FEwNr|-6h8L4N_h1(HdOjP2I`O|t< za*GVX&7}?1J2e#xH}U;b8hiAM{de}Ocm6zKAD9m6xwr={(=wd;Lvl0S{w8*d3)wxC zM=&pc*Jx8Uu=VBUnn6V)@f2-7;g5{Llg z-WJlrFQb$3a*HUL!8@7+?sTT~oD%W2a!CQmTUfQJ*qx~lHBH#4?F)MpDiBm!I!Y(u zrJXu!>zZ)tU425d&-~Q44e?ogVCljW*$Lq#&S^}xeQN6*Ruz)GUCSiylw4nIiP#E3 zeAp6K+C{tE}E zwl(%^U(e$3(cHy1^gJ%>&`$bUx3W?2HP6R1+O53LU){(FIeLQ&=deg1!Kd6wF5>1i zes~>W%xgc8?TTTCjQcgx7~n)i1KP`SVIYj5?s$|65(jbJnv9J18rNxK?+AMi_W z(s4obE#nG;BhAg7L(fB>u*HIe`b(CSq#Yn(xL{|ajOR_XzqsZZl)2%wMmsUJ4bX1? z!kQaTSGhtv)*K)DoZL~H(~|6#D>3vcB>F-sw1~o07j2S}LGokhrg+X}SyTC5!RuSv zxW(455{<78bms5ReE8QX25hM_q*>IM^hf(Ai`C^b7)LxR`mdY*j$2vuExw}2QhLwr z{G1uPB&W*0UlRU^OQ~n^b6?-y!fQ{Lak2rhFITp$*pIDo#yX80o4#%mObpJPtgBM- zwZ)pZLB?2Ny1v?t6l*C<;heOJulfy;-&lIg1S}0FK`Ez06e=sN7p|EEEO8KA0rEWa-5CfV?61Si9ar=EVLHho1y(bpqp$<~>{1%Xav~o%N7C8>ofc&oq86 z9;oSW3V>|(E#@1XCNDOo=F20WP%so_ocjq@FZZs#os^!QR#KVk@jfvU;;T~+ z(WhARtFpM=9nI2;rKF*{mFj`Ms85Xj8egz4F?nJJ^tKuh#SG#!IFgBfo>|e6SL0)S zKNGgpxjH&FJBv%KHCHc$YM}JKVwmuIZC;SHS*P$}R_Vmg3xrv0MQVpLfbCqB7anr<600HSy4>~ZCa*@uHKFwW@Ni9%6%RQ*m}>HsCtTAQ z$@%hjT(-}|t#XfQ#>YAx$0|Y)eTQA|CLug*d~j{nH8AJa<8TIrdFsF@#k!fNQaPqt z$+jHKMM8UZCqDOf;IH(^hSnO@J3~%yfp1(pTed4oM}5U+N&J=PAk=zLAMdFT1NOj` zcD7=}#!2ri_~~#VFlS1Alkr$Qv7gN0_QzRn)gjFVwx!!&L3ngTrDEE#{6m6PT&;BM z8%GTO{UkP?W1F-lCEK|;rx|A#0~7Be_o*R2+kuD)OZ-1LZ4gdFqKOHv&XiBDdeYhO zIe9`;!E#d8)JjjOxPoKV}b`RV3JjmnWnt(jDV zf)t7ros&9Ef}HVi|3$`0Q&M+#HgJx&fM?Bp-ZsLY$# z2YAUldbk1$Vlj?JYZqFeD~HkkWl2)vcu z-S>S#kJ3vQ`1t6$+Qz$)Z7;K)dnpiykc{j_t?|(AlFvY0k9ASMZ{&8TIDtLX?g!-1 zo;r~?j|oo<`FS(b>xcJfj(5x%-~1UII{e~r-VLnd@xbJ&wsZfc+wm@0#oRYLspaXb z6p;)xP~k7Kv^>})ZGU~T0Vsyrre!@eh%WY>Wcy_M9Y9MM8b3iJR8H=meO_*klMQli z#y-}nuk&*gM*)os_H1;PfMGyo!#_)bN;zHWx+wxLWIKo}4r*1}R9tEPjW=SCe=N4L!=LQsFa;L3NW z{YHi%35({tAig}qge)QiVj|zyc)M~TZthQIaK!EgfHCRaA_ARNE?#P}V*4h^f{(re zGSqipTjlsy4MtTzuMX7ENmZr|_^bWAL`q!yMa?T^F>NjbvtR%m9;{{t$wraE(ZXErl>F?q`?`zOGi%6OzPJ z+Vp=*B0C>eZ1-?|Sg7NW@4mSZe?XqNP2F)QUX@nHNa+jDuOFCnr7ngS1Nj=_ zCtsBaYqwQ?)%-0(dyNc+QXec)X^F)LJI*OA7PHwNJB~NAx1!0^gLfy;gj`P|wXGdS zxz0jd;UQ}k$DGHlNggfl*J19S+A5l^gqvpMmKwosG1{;FPDfj*cL)yNO@my1Squj| znS%f@{csc=3!hLtL_RlVcfdS&e2hI(E-}eTunh8h`*oG7k;(QMt|$t+$8qa-OH=TWeRRmVTYZKzLEYK(`uuod`IA27~Pfq^54j6 z!?b}Uy|dZHrajI20~8m16YJ8Z6JIBDL%}M(e(XbcpV%jXbXF-6#4F zTy2GF0-o_G-CnW9n!99fTxV=WM2em_DTvW(Wq=1|xPIIuTg@wcT$t6*tbbhngSQ+= ziC|P2>~TD|$c(ATV?SSp3fc4LjP;p(g+eb1?>&!R`_D5(>gwvOk@VUIe%x8@`_7=# zPcDQlnw4L(Rc5`Y)PX=cXgQ)0zuhJs9M%g016`KZ)`|nK*Hcf6d|qbfxtSeU#hG?L zfpfYPSyri<0Lmwv9=nDfBi*Sj_iG@h#o*dib6{@{v(9oRoz}~@M7<;JCN;Kni1LZD zG(CTJtY!l8ix+^aX>r149dzaesJbIDN^^yk{)@$X)xQuN2E2R!_o74pZ$VJng}ZeTcS0Ic<~igb7J; z5n+#K)PgCG)}Fi1K*xsVhn?7StrGXNfE?yj-hN8u#9xqZ~8A>3Re_KD7I%d?E%gl!+2JbV~$mQ{z=TV1{%n#+MB zO3Rmb&s_)Wd6|}i=vFpt3d+lCkRRh8{wjm9EKOZKKXcY`Z^o=iO?8=9B|udh{d()Mnph|2-Eu4I675&}BB}*sWOrvj_7Q;ny6 zwOkd?6;b+cX{?+v6U4RKcfT1UdPu#UoIbMF-5V2wCB*RLNM@GD+nh7gObo?ESb=32W3L?Uc3}bQsI0{R5 z8e7%oqu*lrHs&Uviq-v9-i0>T`+Esow9kzoJlovq_vLk+Q2Z!QvYnENBi{i#2fnri z`SUW|*l=D%5ZUSQhUD00=Se=^K!MVssdshn9Os1A2 z7yGnwVm(3^7m;EqseiBChoY*>T3d|n+%2n&z~E#twZrw(<=QE_C;s=#0O1&=za~@n z;n{i}3+W%5qViU$(e0#I5nzWaq?R-4f-+uWXU+FtSRw^4`-=PM;Cx zs~%jxT5+G0s;oVCh7K7SrVd2)j~m9GHnzv8q&$6W>M0XnjSzDEZ1hLkW|3TZVoaL( zE#G7DnE>lW<|t+*I|^>XHMUFq%k-bo(y4o+Cika}Lsi$SKXX;|cUPBpxt7TEYuCc| zia^oVIwO@n6UVE|$%`e*ex6W4$h(0yjDP+%K0GnNh!*?iAm7q}sMtM(GtJ<|es3rm zdW1gxWaBV7_2HqE{^pNNo2z5a0d=RpBQOI4DF*Gf$v4NJaq>(h`$MH2-moBlmN0iY z`5HGe@@IY}$VLR_^IfP>YT$Zy>glWJB)P9h-SYXXrN_ncdg?ed!?S=xk9X2)JJ6id z6{9PQAC5EV&Cmp40dDTD8B&P;4=dm@a!BCplnWa!AnpY9TCW5^=x_k zauBos^b{gwE5dhN{#iz*IcI7~+JfY@hrKaC|{7}+(Q?>HrJ8B z$kgt-&34xQ0cWOF* zR|Un>a&>(pd}>*6JT9Lc|8uROVlz2Tlw#1|IgZNbGkTkMF7ul_!dc|-{?WVF&JS!S zawEeYW43GQ*O6c9c6iteQtx4#p2Rxr)CY!jRz6;!2@%>n2^ex6TPb+f%D?wLjkMM~ zsDSLQ^^^O*tgtvDa>z*#HzYs3*Is-=Q(9QqJ!hXrw(yHO-tb>H&31iRh3+o$0c+l8 zWs8+BwLZU;A&;|1ry}dGOS3Zs2tVo|OkrmqY8i@G19E%Bd^~*9Jod7TXDf{Y3*REIM@?p$ROl4@= zA`+x%V|AjrcIF;gU&{lEou{0cF!k@Vtpy-ng9T%j%XH#(s{`8WN3c$eyAGF=c1G z8LtAMin-+EJ>X$?#H~3Q{kD4Dr6~By+6K}7{`Fc&<`J)FAvFHuY(*JIM(ms1^j@WyVZ~VEDGz(>F4MjvzuCP>W=>JPmhDU zrVra}&;M1f^GBC^CEND#Y>=Rzqd14+@W~cwx36{1PB%xzXW3H6Zx`8l&)9O(`~<_t zflq6O>m;`5<2}!oZ|gu)qBogc1ZucnvvzSj`@~#xf2BHT^D=(YkxEMjMS(U zxq#5uz%JGxqfcJxr+6#_MQYEDMBiHrwM7x7M37O%v%772w7+Nz{N-)CQ;Zqb`P|!n zFG~C?L80r}%Yw`i+Bb=LG7)GuGPj^h|LUr^mv}sIPW{9udK(CQ)Yc4g*Ay+Ld_DhV zHzOf7)m3+8bBImq$Do`-@f=PHyZ8QH>QR^s%d_s2&XJi=e*yojvmc^LZ?zn%sOgSkM|#aY#=cZ-{zyCXY)s?r;j4Sd0?e=S zpOxP5DGL00?=l1w5URl(>~h|)Q4%q8t5<8m-MHTObujO@KNpOtRlP&e3%MWxdt?kL zR^LyZ10R9~zI8~o6!7k`g3*03+W9XJCcn_?SyNZK+J(B^cOkYQ`RViB@_+1jxKsC8 zcup!neH?xSW8mBrbv0}w$v{sEZ*>3?noz|a$+-^6=2jNk7c+o;kT<*Byg{zo}qF(EHUJ(x4d$nXRV^HoUj zQa=#~gIM4tP>80;(6S8x&O56;wsW3rlbB~9Hy#4CC&vTSG?c*;NF}TNPMtx!!vbWL z)T1&=^)R+NY`0Oph~Jx=Ky9{qwbdcy)n(xfrcVJ53l4B4`l+~&jMz3qEv4Q0Bhu3O zE(guK)3es8J?!hW#1x8<_c=Ye%S6I?o3ZkM$Dq4si5zfVwEyh7=qctWJr1P%d*~YU zlPQNs52dzateg~le6tp-SRiE zb8~~>-*k?8x8U zvbxciP3U_QhTu>muK~7Fonh+QSUC%sPjOlF^G_y)xyrc(%agBU;w$!9D#HK#OhvmQ zc{f7wmv5(aDyE}@wAg$SGwq(onCPx2VLdglXC!k?H#@gB8TS!|OvL;19p~{}fsAC^ zEO6_twikdxY$EX<{Ar1_hU^AjN|<1u+uh9}~NHjUK`A$w-O<-{oIhl8GErq56VfG_uM&y!{K zruL>3?YX(3(=zo^F<$zcI%LOYWFn}hzmCPpe^z|zYHAc^+*CzeJ<+d~|3`%<8+nby> zy@5_8`38TwbO=R)d2=DXTDH4QZpI*?8SZnWw*&%DbsRb^X`czH!_apxo{Uw6^g5Zi zf5V|VFV&XU5t+G3bz)}rd{;m*4B`pB5G=IuEBekmn6cqH4o@omGKthoIChMlQw?>h zWO)SXed#?lway=UK=LFpKX{TuNp!oCH{L=MpB_`{5Qq4{ zf#7ofL0Lk}Mj-V5n^w;JQU!qszTGKJ-o)y>1eZx~RVkG42z3%64U;MBx7PcQAF$I) zr+bdulVA60Lt$I^9;0#xjyLp4^R650kybe;#(<$SJ?s2>GzWPtY%Y0-=kfUoAOSPG7KhYk2b~=$(WQXB2p;{n&i4p<2-z5;dYMw z1|+9C(`f(lZPwrS@|S*xZeHJVUke;Axwy2h73$nCGvkDJvHWxUG1&9_vt(>8-7bk| z5tr?;PQG6wTFK=@6Th~M>=kR0x%RnV|K<5))#YF4;PCu>X929waY%fapL(Xgecnh? zdtvqS@*8g-|MkMz_lPX~h%J4uckCH4_G0!7EIuKIU<=*`wpNL~nzlbk~lof|GIlCT!HzAVEpIVf2xEN|0lfQe^+sW|A$ll&*uLw_AvkX z`nS&hTR4FJvSRDKitj~z+(xvNTmO8 zDx;zND(ZMl5sS` z?)rZY0+!?sT?gkkP^O7!N)p=ihP27Gt@L!#p*V2%UvNeUjt(FqnsX1reuy|e0G0w^ zmIbI8h-va>Y6>{F1_GB+0rTMGG;kjUE`7l1A#k`adLrWE4&N2EAts5aL)TFz2@=#~ zBxnP;4vCl|4r13NX;+6FE=MS7D3ioZYP2nkbZ{O5-%r5jop|sP+Nm4Z)_}tmqPd89 zKCs9~nAM0y{txE{7r;(;5gf%RMog1LIffwsKbfNsC6Mh)@aBnl1qu+C#hzQ03|R-> zZ-M7u;MA1(`B}8*tjGZ3^_P6`R|%iD7@rBiY5+&-#e7C_?k^-Ae*=tCtlJ>=Sg+j4 z5z!!(vro~srBY69K$$M$E4kN^EUIv3xj=p|kkdhL-M>xbCkTqPz5y3j;G0>934sb6 zD0TsUn1jK!&^R3)NQ2~9NW2JF*5H?Ucz+BwzlN1VFtPzU2_QWOs>Hy}Q4k*v*Vf_q zXZZX*?45wz7RV}q3_hf0!F?%EI{_}O!tYD)pYQO!5DrblC+}e0Ff95V<~)MQo$zcW zJX#3#li-ePaP%KI`~fz+f_eQgwF@S7z^iTWTs8E$15Gob&JDQrGTami``*BZ46TE6i!lBHjB13ed4Z+qcSVKTGaW#-3l7&V zjoEZ1L@GWid`@ERfc{REtzuuM+A>lSc~^ow9c5P*#>WSmYwxB7y0hqpdlcupD&isn z{d_4Tne{*a7|i9TM@NMP`=9o5ac0}t(Dca43bJC8W3T_{ZEJ3BthieyxRsTeb|dEU zg-~x>#xDnqw8&&tCE~!l(5C?4%osGPW5n2(v8m$@c8X#`bsNK#R5|JP3KkP`(BfyiyO>a+_nD{5jNi18oA!uIN7)O zW%%zR_coKO$?lCEVYj~rO!if~HDCUomD`poe|$CBt$}E8)X2{UR^2&1UG0i7(f6AU zcwd!YR>qZ%)y_}3uiu3-F$^%=>}!HQXw>4+GJlh^XMv`oinio1!z49yb&&ByD@rt- zLKh>xwitXF87Nl6xmOxM_y3vd+edt{;3=q2{S-f?Ju|z-OE(dW6B^W7&kmyZsJDg; zq6=lR=5)n_$y*;>TG`e*EWf3FN4pqrL4la$R3n*bSn@WEF?bu-eE0#h){g15xfN^1 z(P=J0!nHy|9lMj;Msk(79EEsIsTNnp)RO7X5k_-YnnJna$B9oh-e?W<*`Mp2BxFCX zm^SRetXN8o%sP$gs%|q**ZUZ#S2MKVx1RgaQ!P7^pcJU>T@i*n!PE9iy0nkgx}NsM zZkcF8nb)Kl5-LHQlmh#F4E+$>IvN*hF4X|cx%b2?W@f%=x%9eO@`A=CQn^m=r+D;D zY}-_VK@9pd!sAZgAXKzS|Mabv|8@B7$PM{U-K%5pi;IOhKGcfYb4H1UCix+ch8}f1 z{(0oZUbn$sC+B6-uJt)XiiZx%Id-amJf3mV-!9Ik%GZz^!8zwrs@;vnCyyCuJG>Dx zu&&>HcNK>U^Fp;9DpbKG4Eq<(w6Bt8X7jQ@V$3INH!7~x(WZZpg;*ne?#GOzykx}_ zPtj)(#qe53jq}4EW~I}3pY(1WF(btz1EW?UoDVMLq!y*kNvZrIJqmN@fn~{vl#+IR zv~aa^>+;I;#kd{RtR_z@6DC$%i`=BSJ(oj5D^sl`vxCo+Qw(cI{jqzd|Jp(c+AJT{Fh^LGjh7PQ~hdzNH-T7}IVBxcgIC-{7U%wZp{;JHhoodfpU zKnM|A|7rScIi+v`B=AuNLblpvSpz>jrLwBJ^`(TQ0XE{A#tEL_lo$5p7S70r(inlY z*MZ>%jkB6<83I~iARiUoV?V0WlTgwgF@%YsKC3eJ6+TeYsWZU#YlQiHrSzlU5kCnF zC2%oj*jm*H&0$nzCbhG=(})$3P{OBlN#}#AT=MKLzS&s~OgkU6xlC`Uv+VLxP8Phs7o%YsC%a_Df~&&9}45aY9?H%AyuKq@%GSx4gqOwL$}E zl_<*gznt66b|@fVYMEA(P2O@%hF*}mqbTK7*Z6X~$|pT`8cM0!$rSEmB}o>Gd5~EZ z)Jq?n>g@RnGlh5Ma*$z?thLR=LBTS*!g2Ng%?>XM`%W^ZF!SpPN=~~C@Lqc-eULaR`#RrCB=&JUM}qCRV#{%IZAz zq8&p){Mdc;5N3C*lCC;3L}PN4YR0bLd5Gi9YTve`?Yn@RnPp)L_lnKH=l&r!@v6}W zXZ#&WUPp=h6+`#=+oHVq`tqUs)PIl9vB{8+Nw#E4nJGF<;<$mhdkGZdM?$AoTU>UI z7OqEbTa$g+cBw>(Mm6NsKy4INOx-z_pO)IM733Xlvi(|8fx;;Zo|KHNw`D9xI`(t! zwvMNqp4~Z$`$Jl>(LPIe_6FUS=-(BG+(v4jTuf>3+YqL`xY>)lIx9|_&?jGgE}5gC zP`}nQqE|$xTg)$5J30=}=y|8;q$K1k=$?Mep$zZlhL^@2DcFDdMbFX_0XZ>7R5lHI zQ5>1kUA8Bl#wV#)ovI)8(Z^zc<=Mc?yyAoISk8q2!J$Bjl6gC(lt@&f4P^VgMX{%jnNO^fz8O`#NFuIG~qL+U3eb$L}E1i|q36-KFgcb;q?6jb2KJ&nP>7 z-&tCArJ=-=h%`j${}0ojt%x-@B3QE(@s_y3vI!x#0wDC=3^-ye22Y!c(t{5}$@sDK z#qUF3mz>jb>^Z22`?sKF^(xdYSd7{)KE<{#mO)8m5|ki8tNdE{F}U&y`m{^Mum)O|97lFjW9j-wA!4N8f#GWWdzR zAQ7jT_xg{)Q8W-^XWgWXoppnK>w*_8i*LCjvgONdX5G#d0m#-u6dn8IMJ=OFe;^{+ z`j~N#UVt@Gam1|jne)yruYUWMwok5@9ShY9?;3;)z+Q2pN1FEZvn}31XNu0dH=?M- zQ#W|V!}822(|q$UIJoEgNh$;L*?O(GWR3x#sIznzPL>YXOvwK7nY*{`I+}Y8#hr zUU|AlQgRJzn0Up@!{W=PZCYNLRbP;m0Aza!oX6gCNsU%CGn;(!@DnG#;{ylX`@7=W zwfD6&Z7`fqRstA0_14l&ma+F`lSdxoG`WY}F?8~u-)MQ~yv;wKq3#}r1yD9}3Tn$v zxJ{ERKBIZ$$r2*FYD*^FRWS6lbAy{-H#A3JPyjND#?Knq;Mw<5W9jpVBJCM+hHKw* z?u`8S?m9(DaHlsUfbx;Ypvrc@bWh8AL@gxg|pH}IZ@N6@60;XgEvXpuVPsu3S2w^dfV6kGoOYB>;!T%pWCBbY z44g0$m^KPv6jArx2nq?CII zd!H$ubo^aF(Eyv~EZLtoT`piR@aSQ{uO-VNcx}}lDh5a2(Y8JnY*22V0AxDo50YXp?f)yd3THBI8*mA2 zldd@u7}j_9pS^P$Fnd0&F&!F8)?)l-a^^G^88~jZem!y;@Q-hShpC8GH;5^}5M-OL zIo+qsYAxQBN_Bt5m`ebPr4&hrbs#f6xC>|kHd>0S=}Z6Pu|Pjh_tJQ19qC^HYz_SM z_w08rTi5AXS8^#8+a-qpm8*eUKLFP3DMD~Lhp4WBn-DNb0KWc15R|O_xeI0jCyk~x znbyXAyQuM*U|mB;0d(vxx`NOaq$ z_7t$bNnE~NLu{_Hp(MVg`ziRW04M}R5>N-b4>Q|t6+G`4$S_z7@h_z97S>J8yAn9| zZ@>@R#pc~P#9BxPC9NDOxCG!R9Do2-0B5NT!)m_hm#?Tmb%rO@91(jT1~V-`zXCXX zcIJBuhY(4Tt>$$Bo_+(-DCK4!-}wjY&Aw>nZK7RS^5?&AjnbDy&F6iJ#omvJ;Qh;i z$&ch>22qovmM~WeE&-@9mB1mJ-UYDH{?liO>+^TAf%eD0eZBL)pEnnH<$PeEU>~PZ zCBVxx-|2JcGcirp_ERzWWk!R=>flPj_4S&AEzru%9Rxmh zdiHicV_N`t@C)F%C45aUvj$#Sp7`@29WiWdrNq@A^bv@r!3^+OetKhvoy1E&zT#8Tj)w{wd>KvC`4gb7unw zKco*v&0tWu1fI=_f(IL?3 z&jY_d2Kb-p#;1>Or>5;oOU2JUurYHe%vVFX1dt;5Y4jaI6w-0ajmH>If9FT^p?)N; zJY8}BJUYhk)0cRz?km6xTe`mL2;iR2M3;=9v>yS_>NJ-C3T$Zvy8ak9%ngfq>KK6y z^m6&~ck=QI-G5-tna0dj=Dc$M7ouA%4J|Ev%@UUY;wkY~taXP|hY|j{hPE8>1e2!kroZsQwW@(DnV3-18fI(mNt3(U;SyYOX`Csw+_ zAOESlij6JjPkZr1!2-}U$W9Z-0CtNVqE|RCI>cDItKQJ3Z+jZngkS!uzWT@86&yE0 z@A^c3k5rOx=8A7BI4yRF&A$B95&Y?xn|5Undfs$|1#@FH`K$x^(y{Ju`e@+q%f*y~ z)&0U^E~jA5sbDQ7w88!5e)>b;<@OInmO>0{M1SUZ{#M|GQQ`zZ=zqaRKHHK@09Kn< z=oo;_kMpvW*0u$FyPtBxsycll>M+g?b4LvoSqcpfua_$YUxz`&&RiU0AZF0L8@L3JR`}GlJBAkPrVtNo#Q)~^!ipfA_t)yK z>b$V&VIcyb&&ST^55gw^r%l`tV1j|YMc>h8wlT+!mONCo&=*O~)AB;TbfZi4RYDO& z<0j4nL=#y@CHprA7}#F&4TrmPPr;UYc?B?3kQWlGo^B7{5-67d62v9MeVQ@6AAg#S z;`C=4%RbXSvJovzIJTYahCxOAY3j^ah!D(HKYyC;?rZE0 zp&;LiEtdd-5w|ciu*ksnlAhjs*Bx36%<2{Jd2#KQ(jqq+IX=%NfU*)Bf`YmLBR+pU zvfYZ4D4!M9j#HqUMJR6KL{KQy0RExp7o7aVF5=2@2~1HW`@JV_m=yAAMkL zV#*+zfkpc29xtL|2(VY<51F_a)arKvWe~EbWp%x=bSLcx{OxNIB@oYv1`o{n-r%$7 z2z&JfcFDXkKvW;D3TXq_q6zab~O8fW+hE&&u4I?yI) zXCT|d$3!r35MR0@_R+Un=Pv3-_h4%q#~bi(WGkFT=0rLpCY!i9R8`nP*F;!xKi$AD z0A_{Ua5Qk)tD;IFhU<^kL&ov)iSgrpGC)w~a+(((z-e`Av;z<(fOpmae>$E&-Gvi@ zzs>_T<}?6P=deAEorRh0ig(wDEQ!hS0fO-+Xrjyj=*=H9fp|BCGEZXWdi9H^g%u?$ zSWE*Wi(4$jtmt^drp5-5$BQJVS0Q3t4CpX z5xyOhg>+%TpN%|B=2|}Q3gF~tbJr5g91Fa7uCb+GMmz=FD?By;!{qn?O590RpbC#} z!`!K#SO{Eul(F$8o(dH>sa9RJ~c z^%h9fy?15hHvJ$8Gci3rKr}+TfGqpKF7J`j4_jD#9le!ctCNLtdG$izsK+w@okjgJ zY;T!aZNbPDwsjdx=(&9)@PoiBN+eb?*|kD zhYk^YF9vhd{^c`*y?#1HQ%#KzK&=2e%n)w|X5#A_Jt$-4CE@}$Gnn~S>iM|gSd;qO|9!N301=z`lKhTzdb_Z`>EVd=^W>_#2xjl=)45m|_&2cmn zRaNW~-}YU;-NCKZ(E5xqxhMN6Oa#Y00la;QxJ{G}Qv=@X2T!n>$($c3sgYzXW?~D_ zRNt(@r^LJdq*xZ!0EawGN9Y#;2MIW|HdA1#Y$J>BNt|{ z)dh!*-Xvwh={jA*22qF22JW4%$M4T(KV7EalGz4sW~+O*ybnA)-^`hkE>IJZmZq3_ zU4T=PHGev4R>b%&VY`E`f19=h-vLj|1jZON*)nZQ%XE@hcVV^u>bH!!x{HLQ69FZ< z**pQnqO=Q8;)0{`^TP&K2R;DIIuyA1c%Xta*hHp<7$Ysfayq)dnb!J`*7K(|1Cow3 zdkd_ZX~)J{5Ccht>Y-_ByK7Jn!shp%T&PbSpEp4dINhCHIc#b;(@fy@ux=W5Weu>C zKG?wYk@=z>I-iz6cIn0thrWYh++Lj8v!*wt_w46mixquq9x2u>m66H*oU) zy6bo*URu_9K6ic$+<(f?|Hj0|{1@4wQ|v4ubCz^E zbK8fLSaY@ADn~8J5O@qhPK!7IYa$*;q;3t0_C1U*ZEo4(*AI1I7Y}on>(^dKxNfc8 zBE|f{ja(vFiDRT2H>PBdcZHG+HNoZ; zw_iMuangwp3DzzSZdt?aYQil5TXdbO$_F(mt?Rx-*&#D|)8{dPw)J0IiBfNguHkms z=N3SqWhMF~FKkOTt*q2aC(HnmO`ciLvsd+m_nJpBvh-Wk>*_wi=W)Z3?zJOP6VN3*W%Nqt7#vPo6AkT>73A zZ(iv3H~5>9YYk;#xc_e6Fjw&$bbC#>Y570Y!Xe{OFeHl;b@Ol&uCF5Xi(V{tS*y~k z-!{CMZCC(-KqI{CzX-cVPyRkq{lRQ)JIy@&0xy3)ISL$Io~ z8~*N{kktr2RQIY@F?eW0-0EAHuBvzbjb_AQWmu^pn{M&$z4BhDkF~*S{LmhD^I01%nJ63YVu-BK6gz`u(SnXqjIO z$M^B2562>*?9b2Sgd^VjjOjYzvQa7{94I`Q_+9YG)UIHAjbC|jH;xu zefejk;hQ%lHNLC>?jM>KJrqT%N=Wv?X7O6-K*ay?ZSesUZ(i3RMMLSvC2g92(+c#T zd@_8)$Ad@)&Is+rFsGZq<~68a^e%}tt&p|!%ArnIZPT&`lSpkAb{nD7xN52q+ZNxL zDn4LVjdjpyYMpMc(`r7b)*6^|O@MSJL7u zKcZE09aJTG`@2F2ds0f=t0dxXB-&^zK$Scu)glJYm0&OWGTl&(T!T}Po z5ru>ii$g1LNF=GU2P+{QjS;8QqFH4RlBqbcT2h*9wIH07h&3r|Hm6Edg+K+Ze5rm& zX)Ul4D9aI`gK&O zS}Z~|nY5}TA**gjSZ!+zs!9I_+G_^1Z9(R&XkJb4j2>IdZ)^OH{b1Evi`HUC&B#ng zC){v{e~a|j%oIB!oBQvfM;rW`yfa5HCoh0rPF?`LoV);fIe7u}a`FP`<>Up>%gGC% 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 403951ea..4e0fd774 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 6eed43a7..bacef699 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 e718e438..68787c86 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 c028ed36..8126e008 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 bacef699..6eed43a7 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 68787c86..e718e438 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 9d2938b2..cd8fba24 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 8126e008..41fa7fc2 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 842c4776..5e59a86f 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 8e6d622a..8eddaf0b 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 11ce96f6..3418ea9f 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 9086c809..b034c039 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 e7bf83b2..8f4275d5 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 15ef1b84..31688508 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 aa752f54..097091e7 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 8e73542e..53f9dca0 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 5ae8e0dc..f86e8301 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 f4555fa3..6ae39f10 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 322c324c..0ec6c52b 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 65368bfb..8e8a5ed9 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 01b30adc..7b56202a 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 48d94c26..dcf62ff1 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 467d918b..085e74d3 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 3b6f0285..aea9acd2 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 9a69d154..28683c8d 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 148723a5..3009e9b0 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 5ab0e6af..7a744553 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 b996295f..2062b57a 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 cca53c83..1ff78818 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 3bec54dd..22565205 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 413d3e16..57c528fd 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 baf3c372..f98d7f00 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 1c530597..35844498 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 6fc94ca2..d35cbdfe 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 4ed9b221..1e2068fb 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 cb68254b..370c9fbe 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 78b611ea..bdba09b5 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 044e2e9e..84073976 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 e718e438..ec2e0a74 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 6ee57ef5..ab7728af 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 07944649..3598b2fb 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 0eb40383..bded6d06 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 986d93fe..def9c82b 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 ff6d6c00..f4ecbded 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 0c24fe7e..278f5861 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 2dae0325..699cc449 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 ab7728af..cf7de0fa 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 699cc449..eee4c6a2 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 83928c3f..7ceacd53 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 07944649..7695bc51 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 7356c6ec..49115cb3 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 986d93fe..1f4fbb8f 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 57c7fe4b..c1965921 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 d9684bac..aaf52fb0 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 66c5ce80..71b2c168 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 b778283a..2e0d56ea 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 748cb39e..a2f6fb82 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 fefab9b7..9c0ad977 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 4a2f6164..8658b736 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 b8473072..db1cac07 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 9e004376..c9f9e90d 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 7a744553..22f938ec 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 b2566e26..95bf0b18 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 ca354f29..c29ad64b 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 1ec963f2..4579ef22 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 0189ad9e..3d08033b 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 95bf0b18..b2566e26 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 c29ad64b..ca354f29 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 7ceacd53..55124fbc 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 2d0dcacd..2fb40997 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 49115cb3..d2f671cd 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 4579ef22..53ae2f5d 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 8d41945c..3187c634 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 fbf52403..7a17c3de 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 15a1a23a..c193c065 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 8658b736..d5b56bc1 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 ebaa160f..c2ae2b6b 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 53ae2f5d..ff7a7212 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 aaf52fb0..4b4172ff 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 d5b56bc1..b34047f8 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 b34047f8..714502e8 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 cf7de0fa..ee7353c1 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 510ce1f7..d1d96199 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 df87a0ea..06300cda 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 8f4275d5..3848d192 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 224052bf..550e9ab0 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 714502e8..89a962f1 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 06300cda..e4a7e1a2 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 517dc975..64d289fc 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 8bea9910..ce4be611 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 c2ae2b6b..d40c66d1 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 cd8fba24..ac3dba29 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 41fa7fc2..5f03ba75 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 5e59a86f..43f3b423 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 8eddaf0b..5f9e4926 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 3418ea9f..a683ae44 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 b034c039..7f92a9b1 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 3848d192..50514975 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 31688508..7e126493 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 097091e7..9b50c8db 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 53f9dca0..82e90a11 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 f86e8301..f1b231d3 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 6ae39f10..e7b3c2cc 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 0ec6c52b..ec7e0731 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 8e8a5ed9..a65f3d56 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 7b56202a..8f7167df 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 dcf62ff1..1651beb9 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 085e74d3..0b0c454c 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 aea9acd2..d327011f 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 28683c8d..a442b585 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 3009e9b0..b90a21ce 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 22f938ec..f9281513 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 2062b57a..a6b5b7b4 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 1ff78818..1cabf9bb 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 22565205..6bfdc823 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 57c528fd..cfdb3712 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 f98d7f00..5d25b6a1 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 35844498..0e77eca0 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 d35cbdfe..da4b7fba 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 1e2068fb..717072bf 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 370c9fbe..0076a7a8 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 bdba09b5..980febc9 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 84073976..8785acfc 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 4e0fd774..d357c9ce 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 e4a7e1a2..4b6cf2a6 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 ce4be611..71dd9a5c 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 a6df6dbe..fb42ce84 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 f4ecbded..0178fe9e 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 ac3dba29..f2210f97 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 5f03ba75..00d62946 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 43f3b423..453ecefb 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 5f9e4926..dcaeb3ea 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 a683ae44..5b68c0e7 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 7f92a9b1..0c7f13d7 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 50514975..6f866845 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 7e126493..72cde49f 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 9b50c8db..19b932d2 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 82e90a11..bc25ab6c 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 f1b231d3..49ce8f14 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 e7b3c2cc..0fa6e029 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 ec7e0731..6edd4a46 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 a65f3d56..35e20d7f 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 8f7167df..d03b0799 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 1651beb9..2006c67d 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 0b0c454c..daf4a784 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 d327011f..64e5e931 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 a442b585..0f64ae67 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 b90a21ce..7e209dff 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 f9281513..7ec6c155 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 a6b5b7b4..a703c079 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 1cabf9bb..16c948ce 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 6bfdc823..285a5173 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 cfdb3712..dd943e0e 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 5d25b6a1..3050ff63 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 0e77eca0..7572da9d 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 da4b7fba..535e4e77 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 717072bf..80b384c6 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 0076a7a8..f5d9539d 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 980febc9..37a7d6bc 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 8785acfc..d78f5aa7 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 ee7353c1..2a4441d3 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 60e81604..b4ea14d0 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 ff7a7212..5b1e0c37 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 00000000..21f08f53 --- /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 108f5a5f..cea1e94b 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 3d08033b..95449e61 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 2a4441d3..a2623ff1 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 21f08f53..1a36fb7a 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 2e466815..1687f348 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 517dc975..4f16f822 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 1eac1be3..78e6ce6a 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 78e6ce6a..b2df5faa 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 1687f348..1ce9dec4 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 4f16f822..517dc975 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 1ce9dec4..85815785 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 85815785..dd71797f 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 dd71797f..f38cdfb6 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 f38cdfb6..f7889d00 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 b2df5faa..f49bb270 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 6f866845..5fdb7ee2 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 d1d96199..62f81b79 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 18a0be27..bab88a9d 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 592a28fc..2c037c7b 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 6edd4a46..d84b56a8 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 278f5861..fff5e2ff 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 fff5e2ff..150121c5 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 c193c065..ebb10517 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 f7889d00..0e012731 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 a371e8f5..666c9a6e 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 64d289fc..db1721d9 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 d2f671cd..8c37f50b 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 f49bb270..da711bf1 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 db1721d9..2a84dcf1 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 da711bf1..8a7a1005 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 fb42ce84..e0ac68c5 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 a2623ff1..c058ec43 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 e4081d9a..f48b612a 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 598b2cc4..22307695 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 5b1e0c37..b41cc17d 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 550e9ab0..3af189ef 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 1a36fb7a..7b60ef63 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 5708e35c..3498decd 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 d1077e0e..c926019a 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 cea1e94b..04284056 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 c058ec43..7e22e084 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 b41cc17d..67a243ef 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 0cbdad92..9833dcbc 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 2a84dcf1..5b418bcc 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 8a7a1005..d032719e 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 55124fbc..d832db0c 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 5b418bcc..d6b1cec7 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 7ec6c155..54b064c1 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 5b68c0e7..2d6d3d06 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 5c4af56e..e1564136 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 1e9af30e..936b9e34 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 7a17c3de..06656020 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 04284056..97b46bb8 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 7daef8ea..39812cf9 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 7e22e084..9f3e2c74 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 d40c66d1..d001dd38 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 39812cf9..94e75959 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 f48b612a..1fc97f41 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 67a243ef..86cc9d89 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 d032719e..aae4c6a0 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 cf2de421..d6885bfb 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 d001dd38..5dccd905 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 d4d803ae..d610fb84 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 381e3df9..de213ae5 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 94e75959..98e355dc 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 5dccd905..ca9314c4 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 de213ae5..109fc1e9 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 89a962f1..99d0e888 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 72cde49f..dd1c75ba 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 02b156db..496e8184 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 9f3e2c74..8236597f 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 1fc97f41..c95c62fc 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 bded6d06..2fbe4009 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 22307695..cfbddbaf 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 d041a433..55920e32 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 83ad7629..5ca284ce 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 4b4172ff..d84789d9 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 bab88a9d..dd35bd22 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 d832db0c..9b00b481 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 d6b1cec7..36b9504c 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 150121c5..4080f9c1 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 86cc9d89..a2ae959c 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 2fbe4009..7e9a9879 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 4d9fab0e..5e2b85f9 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 aae4c6a0..daf7bfe3 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 00000000..7c621168 --- /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 6694d8c5..7f40b333 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 d84789d9..3e248700 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 2e466815..4bda68c2 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 64d289fc..6ad03046 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 dd35bd22..b61ce79a 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 d8f83c6b..761f8a61 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 8236597f..41043069 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 a2ae959c..cc40d962 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 d6885bfb..628bf502 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 41043069..30d38b8d 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 cc40d962..c19adf75 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 daf4a784..b7ccbdbb 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 19b932d2..3b7f23ab 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 a2f6fb82..c783fef5 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 2e0d56ea..17f275c2 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 55920e32..57fdf2c2 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 761f8a61..b4f1f6bc 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 f1114f91..00000000 --- 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 0178fe9e..bcbea994 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 109fc1e9..bef49f13 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 e4b667d5..d340021a 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 e0ac68c5..08a8de74 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 bcbea994..857f9489 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 ca9314c4..4b671ff1 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 d340021a..34adeb59 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 4b671ff1..d9f67e56 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 857f9489..bac1e5d2 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 34adeb59..2ee3bc8e 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 08a8de74..b2cd0f2f 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 e4736c0e..005b2df7 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 2ee3bc8e..1924cfca 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 bac1e5d2..f16c9af7 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 f16c9af7..d568feb4 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 628bf502..34a67395 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 8763194f..7973a0ba 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 005b2df7..248cf592 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 1924cfca..d5c2103b 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 248cf592..e3acf3a4 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 c95c62fc..99130f89 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 36b9504c..1e5723b6 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 b7965f23..da486506 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 71dd9a5c..6032ae9c 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 b2cd0f2f..54796a93 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 d9f67e56..10fd67fd 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 f2210f97..19743515 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 00d62946..c74f352c 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 453ecefb..d956ddf5 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 dcaeb3ea..9e771567 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 2d6d3d06..a112385a 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 0c7f13d7..342eac51 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 5fdb7ee2..74acd8c6 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 dd1c75ba..50e88322 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 3b7f23ab..9bfdb6b1 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 bc25ab6c..a569b750 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 49ce8f14..e28294de 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 0fa6e029..ece6c923 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 d84b56a8..e252219c 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 35e20d7f..036bc8ec 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 d03b0799..6da98384 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 2006c67d..459139f5 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 b7ccbdbb..483879d4 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 64e5e931..cff00333 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 0f64ae67..9fe5eab8 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 7e209dff..36e2a99d 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 54b064c1..31f24a5e 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 a703c079..8cf858df 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 16c948ce..0e2208c3 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 285a5173..44159fb4 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 dd943e0e..892b3664 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 3050ff63..619a6850 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 7572da9d..f0458b11 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 535e4e77..f61ba325 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 80b384c6..cade148a 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 f5d9539d..46cc90c1 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 37a7d6bc..7c355edd 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 d78f5aa7..f7640ae5 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 4fc5db74..234c9a4d 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 1e5723b6..bb207993 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 54796a93..d76f930c 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 d568feb4..af8c1048 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 234c9a4d..73414e40 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 99130f89..26e25a20 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 bb207993..9864947c 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 da486506..48b99943 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 d76f930c..649b180b 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 af8c1048..a284fdad 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 19743515..e45dc5fb 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 c74f352c..84bfcb38 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 d956ddf5..ef9cd7bf 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 9e771567..32aa1f0a 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 a112385a..f8fac073 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 342eac51..4aa2be8d 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 74acd8c6..932936da 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 50e88322..b8c45fbe 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 9bfdb6b1..64a8b4e4 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 a569b750..3918db55 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 e28294de..edad7ecd 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 ece6c923..1b2dc4ad 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 e252219c..27432303 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 036bc8ec..ae375b8e 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 6da98384..417f88fe 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 459139f5..e852278d 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 483879d4..4cce52e0 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 cff00333..29252926 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 9fe5eab8..8ec40cf1 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 36e2a99d..c4f798ab 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 31f24a5e..949eba64 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 8cf858df..7de4d10c 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 0e2208c3..bf30f96d 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 44159fb4..db560166 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 892b3664..599cd651 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 619a6850..c0616300 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 f0458b11..282b564d 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 f61ba325..b2bee959 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 cade148a..b6efeaf0 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 46cc90c1..eea71e6b 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 7c355edd..f0d85a55 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 f7640ae5..5e400957 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 73414e40..1e784850 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 dd0b3554..e3f0c70a 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 21504d20..541d3a14 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 a284fdad..9117c8c5 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 c2d5a81f..9cbc9b15 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 10fd67fd..31cb07c0 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 ac3b32a4..8fa95ac9 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 d5c2103b..20cbe0f8 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 9cbc9b15..70d50619 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 70d50619..3e6409c5 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 31cb07c0..0fe6818d 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 30d38b8d..df2a75f5 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 0501c298..71dd2c96 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 c926019a..96616e8c 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 97b46bb8..21e87032 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 e45dc5fb..e6592787 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 84bfcb38..bcb2c3da 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 ef9cd7bf..d16e3abe 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 32aa1f0a..23884b99 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 f8fac073..1839edb8 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 6eed43a7..37c08a97 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 4aa2be8d..aa882987 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 932936da..da13843f 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 b8c45fbe..7664af99 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 64a8b4e4..db49b5a7 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 3918db55..5312e638 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 edad7ecd..2f6c490a 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 1b2dc4ad..7b932507 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 27432303..31864b22 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 ae375b8e..5f2b68c4 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 417f88fe..59cc9fdf 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 e852278d..8a939764 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 4cce52e0..788aa8b6 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 29252926..c6899ee5 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 8ec40cf1..cdac5f68 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 c4f798ab..5865d020 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 949eba64..fe1de2e9 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 7de4d10c..88f09313 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 bf30f96d..f78a6e9e 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 db560166..63e834c2 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 599cd651..33355fd3 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 c0616300..8af2ccb8 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 282b564d..1abc20b3 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 b2bee959..17314382 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 b6efeaf0..07227533 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 eea71e6b..8c096890 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 f0d85a55..1934a8eb 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 5e400957..24c0d900 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 9864947c..0df962cb 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 e6592787..4404e178 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 bcb2c3da..08f6824c 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 d16e3abe..a2a19a37 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 23884b99..905f4814 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 1839edb8..4028e333 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 aa882987..fe3830b9 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 da13843f..b9b31f10 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 7664af99..0b92c665 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 db49b5a7..4965f6da 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 5312e638..e40151cc 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 2f6c490a..0e1887e4 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 7b932507..689ae98c 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 31864b22..65f91ece 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 5f2b68c4..33fb2da0 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 59cc9fdf..c874dd69 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 8a939764..01014bab 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 788aa8b6..9dd005bd 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 c6899ee5..716d3df8 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 cdac5f68..c7d0cd6e 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 5865d020..2d48b91b 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 fe1de2e9..8224cd5e 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 88f09313..5e033095 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 f78a6e9e..a75da46b 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 63e834c2..d3964a2e 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 33355fd3..78059645 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 8af2ccb8..ca225775 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 1abc20b3..4355d643 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 17314382..57dfe6e4 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 07227533..49a42af4 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 8c096890..50e68425 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 1934a8eb..f37ed341 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 24c0d900..5788a7f3 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 00000000..03491be6 --- /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 0df962cb..0004c65f 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 08f6824c..65039f0f 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 9117c8c5..dcfa7b74 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 0fe6818d..1ecbb064 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 bef49f13..616d9237 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 48b99943..323b464f 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 649b180b..5911c40e 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 dcfa7b74..67946f54 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 1ecbb064..15bfe90d 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 20cbe0f8..c3acae9c 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 541d3a14..1b0d172b 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 1e784850..147cd914 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 323b464f..ed270638 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 5911c40e..2ea33b65 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 67946f54..d0e72a7e 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 15bfe90d..e2833294 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 e3f0c70a..009995f4 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 1b0d172b..5d6692c3 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 147cd914..2f682752 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 e3acf3a4..20acd811 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 d0e72a7e..8f2b4532 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 b4f1f6bc..7062d85d 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 d610fb84..18f61884 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 65039f0f..5a9abba9 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 c3acae9c..1007c71c 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 5d6692c3..eb83890d 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 2f682752..a740b373 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 1007c71c..87b3f74e 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 e2833294..588733c3 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 87b3f74e..c4c9ec16 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 8f2b4532..e34df30b 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 7062d85d..f8d8569b 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 eb83890d..9888e583 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 a740b373..4b47608f 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 f8d8569b..e83beb03 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 18f61884..0ede560f 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 c4c9ec16..da012621 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 2bd8824d..dc941c3d 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 5d451e4d..1120a173 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 e34df30b..d4922786 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 e83beb03..4249e4d9 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 9888e583..999b409e 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 4b47608f..f63bbd08 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 e1564136..52fcc76c 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 936b9e34..b315024e 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 00000000..0637b58d --- /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 00000000..fd8334f9 --- /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 0004c65f..d06be52f 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 18a0be27..61602c5b 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 daf7bfe3..2a4c6883 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 52141364..b2f70cdd 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 d06be52f..653ff37b 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 61602c5b..14af9657 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 2a4c6883..a2fe205a 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 56dca4cd..6cd905c3 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 d4922786..aa51df37 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 4249e4d9..a27a9d4e 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 588733c3..cfca0e08 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 da012621..1e88b9b0 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 dc941c3d..cce55315 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 1120a173..ccddab0e 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 f63bbd08..cd0bdcde 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 5ba29c8b..00000000 --- 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 00000000..87fc6a5f --- /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 01de3b33..7b43e397 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 0d21f017..00000000 --- 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 00000000..01f6c6ac --- /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 6cd905c3..eec424bf 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 aa51df37..234f4f84 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 1e88b9b0..7a16df81 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 00000000..c595885f --- /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 b2f70cdd..a253b9aa 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 eec424bf..aab12ab5 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 234f4f84..05eab692 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 7a16df81..86d83761 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 87fc6a5f..d3036ba2 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 00000000..a1ff080c --- /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 a253b9aa..66a043fe 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 4404e178..e98c6636 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 5a9abba9..64c37709 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 a2a19a37..70a3eb6c 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 905f4814..ae943e1e 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 4028e333..44bbafda 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 fe3830b9..f457833f 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 b9b31f10..22044745 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 0b92c665..c206f91f 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 4965f6da..39ee3bc7 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 e40151cc..7cb678ec 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 0e1887e4..25562f55 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 689ae98c..68a80e54 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 65f91ece..9730bbc2 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 33fb2da0..7069c0da 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 c874dd69..43eb552d 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 01014bab..49c7b991 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 9dd005bd..41239961 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 716d3df8..e69a140c 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 c7d0cd6e..0887a591 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 2d48b91b..304353d4 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 8224cd5e..1e6c6962 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 5e033095..6f6f7a18 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 a75da46b..2fb74fa5 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 d3964a2e..5d4a6e1a 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 78059645..31a3ade8 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 ca225775..e30c09e4 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 4355d643..b8861807 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 57dfe6e4..1c75aaae 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 49a42af4..a9e2c171 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 50e68425..7c49a29a 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 f37ed341..92c99d90 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 5788a7f3..8bb1d45e 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 d3036ba2..9bf1f615 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 01f6c6ac..ab4e9ae3 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 9bf1f615..16509a3b 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 ab4e9ae3..50cd6d0c 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 c19adf75..b923a31e 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 2ea33b65..020bea1f 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 05eab692..c8a0f2ca 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 99d0e888..03d057ef 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 cfca0e08..84407cd9 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 86d83761..1bacad12 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 c8a0f2ca..96ddd51f 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 1bacad12..17417cf6 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 b923a31e..c61287d4 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 a27a9d4e..2d7d3fb8 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 17417cf6..a8849b4e 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 96ddd51f..f5792bce 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 a8849b4e..02888d1e 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 02888d1e..9ce53c96 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 00000000..e1b96212 --- /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 0637b58d..5654befc 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 71dd2c96..2986adc7 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 653ff37b..dcc53140 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 14af9657..bf7f8773 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 84407cd9..2e6c450c 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 6032ae9c..71dd9a5c 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 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 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 zcmV+(3*q#MP)x<2*sjK7t`rx?s-`o#kyH;F4U<& zmYT@d5k&@7sZp&bU>bUoLf|2aJ=0HGACMOwQP+nlFJ^=YU?}KofMCOSMMZ$}4AtCh zZM9jqG*55AA4Y0Kd0-cf{@Cq3-08unH1P2N5a8s#QSk4m2E&482i@$X5B0hT0?f(Q zEVmd}T8w!bg~^o!#wKlO@$74L4Ui~kIvCG_$n}?Vy}$=)UfJVc_`E;)bSPKL*= zY>aqs#<6|nje)!K3UtW@^TdHKKcHW*Iy;<9z*plOt_j@SPqOW9x*|i9aGycKz<=wa zzI(=reJl`rK%C66F-!(CwrxSu(JK+)W9{S$tBYbmV9JpUOMuU+@ehi+;%uC#BtjV> zaRVQ@MZRQpvw_aY!w?$D@i5mGXKgLcOqeqy@d5w&4z>EMFE4~~VhAOM=9{IN z5Gs&+#sPf(9=-gen`WWlOq+n|WZb;>$?P{uOvyk-id^7F9`yZ_9xok}AtvOa1{Ie@ zd)UC=A~)}xlBFOmkPrMyqo>=B5_3@%gwKvsFS2N!&Qn)psS1pQ);E{ZEb8;Jt#;;$ zjXL*`v_2%cupgoM%_lOpmPyc(EV9w;SPda_z{>(Yew+S73ll5@;$WU&BsM&yUtOlN z76FzQ<$l6>e~39(N7T*IubrYZsO3f)lbTplWWZlIsmfGCm6XNybl5lF zviZ}VHW&S^>knGyPRNm(x1@phT%mgGF;lMSoVjG8`p^R7cw>TdW{yVs!QJh4TuVj1 zd%u_6)iLz;r^DDhc3yS17v|#Htozu9=~GyHc;ngefGuZrgv+Ivh1w1j7ohtKRK)>@3X|d0Y$_ zdvC*4M!L`oGQT=alWN+Xx9oK-_HY6p)1MEz9b1bsjVeK>5Ci_~CS!9)d#`Y<^ZX>$ z{%2E5d*F_F6mY$QVX?jFSXSD#H9Ps?OfZxc1z}5~n@VH3c?`v$FrP0gI zE|QTArN$Sp49Rh^vjg6^fiHHWC`AHxRztYDBIV}w%XcQ|*QtjcG!pvgLyZeIQb-9u zFv+0*_Qw$NF)y4*j5VgiyjYAXxOANEc#kLa?m`{wbTfk#Yv#+?_<=iYfMR5&R*NR3 zbvtfhp7wZ;S5C3r>ZB`kw6PlayW45=Jujny+4!Fhzt&QXhX421o;(;W|RpChD4 zg1N$+rt$2TDVb`qjzz>UTxs)a;0LyOA|jSBGtR5Vo6-uLr-d2_DJ0ynVB-;mcs1|? zd#LDqLx9<&;fi>rVB`q?h%;2shPx+a3=b0zurH|yHbhkO>rAmAS;i3R8$U3&zAs~4 z&P4(iAtIG2^a#OkXAk9CqRc{DQ2f9-df%_`hs3WM=brseU{P{WQ9|x}xHsi-+8E|+ zf#i=LINt<=(I1GbZW%>T{i_rcy~HaXHgRg(H?IbGa!VV66&Pi~-l~N2(6J7*fss zKGVTENtB^Y$EY6gvY+?SQ4vdkIbR2b&PNDl!j*;aT3=)a6B;cc)!6>Ps zb;>AGK56rk3>dkf#m8Kks}-6$1cAR+3J+e2CKQNa4NXiMfN=r0|ArR;St~HrV+1Q! zLEt6j+@26Kf{^dyyn2t@YRlE9c2vKA)7$A_#a3V}$cv)9S9qmuRgu`yCQ;y7VAEgWuosC$|GH}8^~w}FFhXeKd3R$^aPLxT1;(fX zKP=IUC=>(E(!*D)(66k5na%Dc#OC=a%sMdb*2~@xZjk&P;A4KVSeK_0>ypF*O-(iU z?Rhxl4S5I%Oh^DUd=sv(Od&1Bp!NUseArBiohONA#>vvACynB2C4h10dTl;>>r`a= z!ToyY>#3#faj+{+yU+DTk7S>Vimbq3^Oj{w_?Pq&T+Kpo@FKWp4>RvvO4Q7kh*C6* z93;rr$6Y#lX!PDS>aEM(VM;oak7UN)TR%hkB&IYl_T)dUK`W1d+XFC54|YBeHhfir z;y@F#y^$I8fLRu>VUFs16-1^^8ob|y4&9&{{zG+h=Tw2}< zHwV$BKIUu}bH10QQN$^0KaV`-Ib&2cPl*NR_<;TT^E0KbwBsyWZ)GET%suHQ(TP^} z#4X@IUyNVWYq*9RC&nP+9|b`1^%_Cu94&EVfl21-nv%=t9PL>)Tv`CYYYp6TQ^CSB zc56FMW&)t2)ftnst_KDCT9Ji=kpW=BDt?(aeBnW_qX6uxw%+n_J2N z*Pmi{oRK_A?6+(^U=QLRDEbULH$#UJ0C;+^t5$!k?9Kul}69mV0*q?=&fvm9| z!Cdaok4W66fXa z;gOUQGhAB<)>d*R8gQZ3huqPTJ_jD~T+xd!>NL1u=7M}t4lFmru^9>*lULGo1}D!5 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 06656020..18166c8f 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 7b4d860d..682280dc 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 ebb10517..cd618dfc 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 95449e61..8701d9f5 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 00000000..f69b60eb --- /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 00000000..7c1136a7 --- /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 00000000..75324b38 --- /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 40c19c43..9712636b 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 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 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 00000000..62e56824 --- /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 bc9bacf1..62950846 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 62950846..86606372 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 e1564136..4ac2720b 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 936b9e34..5d75b7a2 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 054a3958..bcb0650a 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 2715a264..072ef53f 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 4fc5db74..95b8cdbd 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 36b9504c..4fd702ad 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 c2d5a81f..8f8ce8de 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 bcb0650a..7d5f36af 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 95b8cdbd..3801eda6 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 7d5f36af..08ab23b1 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 4ac2720b..98836301 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 5d75b7a2..936b9e34 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 4fd702ad..c3c8ce3f 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 08ab23b1..fd951442 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 072ef53f..1d7d4773 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 3801eda6..12412d7c 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 98836301..f5ffa7f9 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 936b9e34..5d75b7a2 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 fd951442..492314ab 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 1d7d4773..133b9a83 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 492314ab..bdf1c5c1 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 f5ffa7f9..98836301 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 5d75b7a2..936b9e34 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 bdf1c5c1..91480ba3 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 7b6130ad..edf0ef49 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 98836301..93b40ca3 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 9730bbc2..a4ea5830 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 16509a3b..c2d92097 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 fe0d3107..5ebc3a28 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 699aa9bf..bfc79871 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 71dd9a5c..1e4d80c9 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 c9f9e90d..1c49adfb 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 0b66107f..ddfe28ba 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 716025dc..7c107d11 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 c001c58f..57d9b68f 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 a034b4e1..2d9b5a98 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 a7ac4eb3..f574e830 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 f5792bce..5186aff4 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 2142d973..79a4664d 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 03d057ef..0af7026e 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 0ede560f..699b0bcd 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 91480ba3..17c52abf 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 616d9237..7807c4fa 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 98e355dc..f34b7c2c 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 ccddab0e..de33b016 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 c53c573f..83f623ca 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 17c52abf..5b992071 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 dc0e365a..87ea8e9e 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 2e6c450c..bb1b8b8b 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 bb1b8b8b..ec4a9097 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 18166c8f..06656020 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 5ca284ce..f03cd0be 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 6b107ff4..dce43472 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 5b992071..28e15158 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 4b6cf2a6..67eb0234 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 00000000..da23ae42 --- /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 2d7d3fb8..bee585d9 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 91480ba3..2764a440 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 9ce53c96..17d4e376 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 ec2e0a74..20228ea0 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 999b409e..2d0d4d2c 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 dc0e365a..a0c4f06b 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 da23ae42..00000000 --- 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 c1965921..35d5a61e 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 52141364..b4d7f4fa 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 a2623ff1..04e29eaa 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 3e248700..32dced02 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 b6a9e5fe..9955c276 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 4bda68c2..c444d1f5 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 b61ce79a..8320d08d 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 598b2cc4..ddc51edd 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 1eac1be3..5e4693cc 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 32dced02..ba7b8a05 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 28e15158..105b8440 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 ec4a9097..ad0d119d 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 00000000..0381ff96 --- /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 00000000..24ba9a91 --- /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 fd5b5955..a71c71a4 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 44bbafda..1743505c 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 37cc77c8..bc31ae2c 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 67eb0234..4b6cf2a6 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 6bb49000..7db7d43a 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 1e4d80c9..8b08e1e2 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 020bea1f..fb255176 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 bee585d9..2d7d3fb8 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 18314dbc..00a2edd7 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 17d4e376..9ce53c96 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 009995f4..414edab5 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 20228ea0..ec2e0a74 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 2d0d4d2c..999b409e 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 f20d1470..96cd9836 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 2d7d3fb8..bf5746c1 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 ad0d119d..a7e32d0b 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 bf5746c1..f60d9b30 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 c7dbd9b7..b61f5173 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 f34b7c2c..c6600608 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 00a2edd7..56e11f32 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 c206f91f..8413673a 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 f513a1c6..2385813e 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 83f623ca..f0f66e28 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 b315024e..9588d10b 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 06656020..0019335e 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 0af7026e..e2f3f80e 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 b61f5173..0c8c5145 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 e41a616d..b449bbbd 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 c6600608..8a1fc990 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 c4b07b37..853f3168 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 a7289335..211d36c3 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 853f3168..956b985a 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 1f0d5b0c..7a6bcbc1 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 2fb40997..5833e760 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 956b985a..9ae85625 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 8c37f50b..c37d0186 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 9ae85625..54b6f1d4 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 22044745..939a4831 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 1e6c6962..1792eccc 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 b315024e..c171e84e 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 fb255176..b98f9fde 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 56e11f32..105b8440 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 96cd9836..d55073b9 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 eb837ba7..f30209a6 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 7533244e..8ef45139 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 a12d5aca..3a0fcc5f 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 fdb6b2df..06af70ea 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 87ea8e9e..2944a76d 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 8ef45139..a2dcbdbc 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 020bea1f..ecfc5974 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 a2dcbdbc..bee4dd7a 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 06af70ea..b6663ad7 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 2944a76d..cbf6d017 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 a295ad4f..6c1245a7 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 b98f9fde..78feceb7 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 d55073b9..73f16478 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 f30209a6..e09a9987 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 34a67395..588c3646 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 ecfc5974..c6e0a759 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 bee4dd7a..bb6f85bb 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 3a0fcc5f..b4e79b36 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 b6663ad7..ecf96ab3 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 cbf6d017..85deb68c 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 7b43e397..2da6bbaf 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 965218c9..d3a3f7b3 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 d3a3f7b3..13eb73f2 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 5cd2af2b..bd2a0129 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 837a197d..e96a2b40 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 c9af6328..3c9a438a 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 2986adc7..cde1e6d7 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 9955c276..27bb0377 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 e09a9987..8cf90eba 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 588c3646..ba62b775 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 c6e0a759..a2159257 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 bb6f85bb..a60e379f 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 ecf96ab3..e44e3140 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 85deb68c..25c15f52 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 54b6f1d4..d1faa549 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 cd618dfc..91a061fb 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 8701d9f5..df29252c 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 d1faa549..1ec57b46 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 1ec57b46..63a289c9 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 63a289c9..951d63fa 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 f03cd0be..1ab21dbf 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 05fc1fc5..14a2c38b 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 6c1245a7..ba7e3d76 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 eee4c6a2..f352c313 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 cde1e6d7..af7f1481 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 80dcd80b..378ddbd1 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 6c1245a7..ca34eace 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 eee4c6a2..71660cfa 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 cde1e6d7..ced8e33e 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 27bb0377..988baca5 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 b2566e26..7540f766 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 2385813e..2ac6bf23 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 ca354f29..ee5dd9b5 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 211d36c3..f9db985d 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 9b00b481..c251aadc 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 8cf90eba..d0a2ea60 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 3af189ef..864659a6 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 a2159257..05b34d78 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 a60e379f..0161e644 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 b4e79b36..3025d722 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 e44e3140..447c2e31 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 6d3b6d38..36c74dfe 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 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 a2it#tsAIaS9p3lv(b>V(zQoQq_WuBm=|rLc literal 53345 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

    ^0>;>{-A-^Ls1s>XX&4yG45lUQ|yu+oo`W-dQ3 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 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 4bed6f3fa9928528fa6e0046af8bc011861b76f3..3bd2b7ede030927dcf652ee5db6131bc94df7c69 100644 GIT binary patch delta 433 zcmV;i0Z#tX2fzc68Gi!+001a04^sdD0J>02R7C&)000000RaI300960|NsC0`T6649sARW5($XWmZ0HjGoK~xyi zZO%mk!cYuF(SITpoYdW2acBSAB27EwjdIVQeCE&CIgFdL)Od-Bs%9P@TLgTUpGIqT z9}!?VVHoVPel?X@07?(iP01y|D!w9tlPcIK?=Bz)G(go3a15^0l3^qS z+QkM7JFg!e0cc?BVWcRf3_Wlt=Rl9sJ2r=~27trK^(q|;j}3l(#;;)9f}b(H?1d>! b*kS(wqNfsT9EuRY00000NkvXXu0mjf_j}yV delta 967 zcmV;&133J^1JVbO8Gi-<001BJ|6u?C0fcEoLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xF zhTo=2MJgTaAmWgrI$01Eanx2QLWNK(wCZ4T=^r#{NK#xJ1=oUuKZ{id7iV1^Tm?b! z2gKFINzp}0{4Oc9i1Ci&9^U)jm%Hx(p;={`)iVKTx@~4s34bw{Ull{I5D-F`W<+L| zF)vAJ_>Ql81o(az=UM)Be~tmQU@;&d63;Qiw23!}XEtqv^FDEum1ULqoOs-%3lcwa zUGeyhbIE0aXGYCzCP^G67E4{MbTKQN8u2u7Ox1MC7jhn}oVPe@)jDh6lfN)r&{vkZ zPHO}yEMf@~M1Lr#qk<}I#A(+_v5=wjgpYsN^-JVZ$W;L&#{z25AiI9>Klt5St2j03 zC500}?~CJni~^xupw)1k?_D?aC4=ki2wis2}wjjRCocUlh11tK@`WoGy9{dra_X{Lam@_E@_D%cnOLuJ=PvA zIf{7l*rWafDn0aWDHHqU&M1E|H;@*PI0A zCM~`BxfvjzH1PQAAr687EBSPs6Jvk0B`TjJU~sJ6PGJ6Wx8gJbw`d+uzO-PHoM$;P z_!qYJB|4`ZsU$FWr8}@_%@AZn8aN_3dbs@nNb1PrVE#aExUC+aQ{yrW`T^I*DF`?Y zkAFyWo!TDz6YzS^NAsAG1OtVX1-BoNKF4tXI>*EXhWx0KBrtQ4K~l9>yB$1u*9HVf z>8eGZ-~%1#rk>wbgJpR1M&Rj(0Li3)GzD59KiCYpQ47mA&iASc0m`1e?S5}CEvMO} zz@|e(WVR_2%Z`n)L|8q_(E#ObWzWcst3V43OLsUn_rswT#u+lh-Rn`UR^O|f7@$0@ pynWVXa=Wj8zf2KCc^m(@egUUy%9+Dj8;bw{002ovPDHLkV1fvh%1i(N diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index 22893b8ea78c8c2d4dc1835a5e19b3c056e11d28..88f2eee49a9a8162eeedf5df764e8842ac2d0912 100644 GIT binary patch literal 6198 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 literal 10828 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 WX8Oy%I)Y}20+i&`WNY5Q!u}tVQGC(> diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 583a485712bcc8691458c7abb38b37906ae6649a..18151e82b11bfd52df745f84cdc3fb0fddc45f30 100644 GIT binary patch delta 863 zcmV-l1EBnq4C@Av8Gi!+002a!ipBr{0b)>0R7C&)000002L}fN0s;U400000_xJbf z>+Amh{{R2~;o;%_{{HXp@B91vM@L5j0s;a80ssI21Ox;C00960|NHy<|NsB7v9Y3} zqR!6F{QUg?|NpPAuk`fv|NsC0|Nj7g`v8*j0J7%*cm4o+{eJ*``~ZUb0I%o(wC3;k z|KRZd0EPJghxh=D_5g|Y0G9CpneYIh?f|9i0I2H#s_Fo(=>WIo0GsfDz5d|!`~Z&h z5Tx(`p6>vo?Etyt0KDVB-2d%LqNkiOPc@y0hvieK~y-)t&w$e!axwkX_{cgp)P18DOLqaixhV$ zQg?U#{=K~r?#T4dzM0J2?fXq`_pRceG^bJ9Wm0RV`?hV&{-;K4JwQWhxkxcd5u!gO z-OH`)qJL{?bN@3VRc0}u7ccp@)wT5vAY$>a-f#5av!nQ2i>~YtBGc=XMF5A7r@_#6 zpE!7y-e3^Gz(HsSfQYv4Zo666BdVNXU`PV4oX$WndSS#~dh^WxO6C{=+O%@Q*pn(sRX_lJe*o_FD_}7s7D+MInZcmu_DZH059rckLd@)M? z67S`Oj#~7>j(W9JYCgbTLecyn06yvi62p55*2p%MaKe2MqJ55QN&q;W2=Ir5xDmvf zj#xneJ0rc3I4Jp!6AW(6rEEwPn6hTX6;1pPld0eZMyGreZ30lPB ptY+h|Oh%K1Q4q1rkW36KI=}E|c%z@Oh>`#R002ovPDHLkV1jA>&Itej delta 1549 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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# zDXd~$Tztf!5rNLzY}ZyrQ{Hnp;M3#Y6pu7SB-DZY2S5ZMmC4US;n&KRr_Oge zJ5NCgKAma1@xh5Cf#dWvC0rm}Nl)6tQc;*BL?bHM5Of-Q_3M2CNEo!st%w=h9M_J4 z!L2fOx#4%P&H4EYDVynt1gHb?#RP6-=iOSz;lUn@-67zU(>B{?*Oc7!U3GX}0yO=? zewmwLdV?ig56lPWF9h)3NZOT!A9x)ANB|h0n%jE13x{L)S7Fz2Kx|S+HJi)^6^U>F z9Kc#P5+lULqM{1wt@(iw-kP!-U>ko^8niq0V9t{(hkJE7g`$uHZ02?@L%DKIJH&hQ z5rSs_W;#c&vYeI}kOKg+b~_qX5f827#mBbLpHgP*hNbA_GlIkHovTp%@laAe{GLZo zsPp3qF!Q66uG}qM+mxU#qdKW#vu3hGX0lm6`4(PyK#$EfrRH%~FvUUj#jGoZ=7XNx zXFc>ZMa=9V-vbU6%?H`ISYYCyY+++RYWF%+!va*B=<_=i7nPYC0e039eSBD5zWv#`n}b zPJu3oYliE$T>^Nc+FN_fP7L-ert{|6z~K`jmVup$-&9u$l!TfOdV(%DM6^4duvi4)Q07kcL~{xw56*%v@VqFJ1}<=#)Cu zO-96^oDFfo(fHf1QCb&(JtkHM@-qp-hC-q+M`ReVjW_=$SBqQaS$ISGGZNKEaN-*s z7+$y}Tqef_Y;L=mn~rz}b)YFijwKrv&_`o-xCLwu!RSO1=5RRIH0LQTF!rR*zdpKHy@g^2RJ(QY8@SxcVt|mlio)!qS->yzY zELAn5`NwVAzKGP3r^T7ZhI?VN&#&HR%?818P6>a!3z3r$02^El^9fP5SXK_2z zUg$wQ#j)yU`*l4PC+dSZp!aDWLz9wgi?t5MKrgT=?YbeIih~F6zM03Er))ko)V|Pz z$AwGYy7G*m2zJ$r1o$m~c*1>lcTlnn`*^!&oQ?ae1lm95QK}17~6R?~s5b)=v(3eme+!>wZyU-$iKCBky$emDv)dFVLr4ig^_GPaw$uAoR=|+he3XZ|p zT&VHMBo6g79_o1;gs?5H0~4Nd&h0FQg%BWcZ${B1Ks~YOwrO=>-nVpMLjMfPN=VTW z?s{1O>W;h;jqwIsR4(+hn=YTBvKB3Bv-Y|KBlACGySfU$2qJbzilp6_4gR!P?~{n1 zQl{;`MScDwt&9@u{V$!bl>p>BN)XuGBm~}t3cv4Vp(aKrU9d#_?s|9tZ_*yT{bx{x zE!Q;o`WdSnJKH(?DorXDI^NhiuCpu=cFS-0fOF00-2o=7B>?YBPe$8w6$TJb3o?wO zX0Qo$3=_70)Uw7g3c6&2oxslbP*H!-0xo_r!FK#B)YAlLwH|f?rVb1qTnNs1!4d%N zoPvmlI0+mB`^emzL}c}8G-Xd7I&9J!-Pu>xN3tr zaIz+)s(@@I&H&gJblnI_Va>mo{qx~N(Ci)n2iRpf2LsbUpYsB7O|?3d^F{oEq5ZN! z0scn#6q{(c9p9qNUpCx7BETNf>phHKLSm~ZKb5ysi0*fv&O{m^=HT+caD=jpkYTMPK$jbKmeMaMXsbT5*VPwriy`n=W zZ}zF#x8&jU%#-(NBTL>Zf;m*y`Q0)1)fV|rvrIO|zE9>C4-<&hT+e?r5~^KF_Nn_W zIe>DBLak<#fL!}c1cVB#h8W@i$845k2wAi7-2Rk4RJxzE*+kCpV-I!N1&^!>{;RO} zuix}|ij~Ra<5u7v!xF4w$cbH!laeT2QpAdq0{K?POtzsK44|5m9T>!(#h#*uIFQ=> zkY8X+$0gKUd1s2+Y%+QH!#*nOglSil)r=VO`%TLOw%MHk!i0fvaR`g)mEmN+tJ-D6 zYuiovjFA@ERImxH^5rUklouZYX!;(4DZ_Ly*$@}j=&bb?YxC*r-0(1GM|8mnbgcNC z>mi`ZfV1m(D+<%1moHKJSsbEWMP6%*m;dcX>h4wBTPgbBlCg_}WYajNO9S-QQjE;T z)BEFOgTorQ>aQ;RRET$|C85Siy+yPgTBsb#GDJ1#y?2#Gv|gF*CF%PRCKZ6!W)|9Y znkxdzsgK5-akp@nMY{mJ;yLgNV}CBOYVN8dcX+SsqY43=0|hb~94Z{L?NbP+HrpT# z5wq!)L3PIK^+Ew0fL`#2lwt;KN(Wh^0|=`R@)_d5_{N$2_LQ|OcCu)K3%Cm;Cut^s zXq>lHc{~Ij0?*_zAV(6bDOcYfldxE^4bI)!f`^wmZ3yL!tW?8E#PZQuZ zV&Y89oxD8u)O<16Kg7b^md_#~j|xe}u6`9}wFk-5VKgduB4M;*n~)}xf4zfPm?TtZ3I&Q0c&s{J6I{bxAU(B^RVPq|U->Av2D*;RaTT}<^Un*r=LajH zsOT^2xbC<1ibUM&4Fu@4O+)c@WzrXc>ii-Aj5V9GG=c3VZWeg2$7AQd*Kcz@b$%YL zonj4QrG)q^%y^YKk;n>fUVUeL=x12^kRN|AS`1y=sKj0 ze-t5myu6gTIZZ#LB|8%~|NfNH4y!`c&qk0WIr1+w9m%VQf9=uThXLDA?Ex~5NT)?^tMN^80e77ZTvf7#c&_S|ucdj@yJwRGjI_VPDj`HQ`nENsSGJ{4X7%k-DJ% zgF&1jACNAJ%^tw-r5muY7Sd??j^s`}V0&d7 z++##zD_EGsMr?DFr|m7#F||Eq--z&daCF;EdC5keQc`uJ)!FV*r&di50V4K_%UW3; z;BEqp{&!aZ3l8w@EQFouMdFBoIZczFr6HyJW4?Kjq#y1f#1JBg$cl7caw3CS0Xng2 zl)KQ?@XH>!zIUs3WrpzKf)d0}Pez{hv)mE~>`qNNR;{LJnu4?qDcv5IaoR9^A_65m zezhEZn9#>|r&3GZcpY`D>A_EAEu5~El_T5vPblSpXNU37XwC#;U@m!Hk$05^PE$sJM11ws zUhBGZY5Kij@g~ZiofD58k9R(|Vw(X=B`a3*LW7t)zad2IEd0a1w6Ke`wGC-2N&#PV z0A;Z3vv zgHYtpz0#P9&iDvO zTxl$EqZrO9)9X{i)B#!dgr+7BRh2M{OSfI+60SyF5C)kWl1Gv^pb9E(bwWOHHJcTf zYoW{olg}G0Uw*RzrHfpe9e)e7GFvR6$gO07Gk3i4%H)4$t(yJR2JC$y%Sb@tLKyZ~x$R@HJ z`M!0F%2J|9jZVRzKU}2t&Y(h9CLT5~ZsHFNy56c z-#2%?v4&3JLD;N}rNV2YG&~5bQEHZc=YkI4&~EM=HE!hKlG*AxIrMD1_mz>q44EAk zoZal@5D&Xg)t+0TDznUrDtEx4Wg)}#zC2hadBk8Ia8)LMFJXi^Q=Th^ioX1j!^gaa zhIL_Shz@$WOoW5{J#L>|-wJh*FXCmjz_f|Kqav2I2@*K62@Vmb0(*Vm?ar}(F9lfB;Wv=qFArxN0 zy`d$Jqz?Af%4*@O%6rE!`eD>=DX;%Ne{)7R2;cdeXSs#T!9UFz`k(Aq3|;9O?m^F7 z)6zU2;lTMx6n3SK-V0#QBFUZ{^E8g;oand;c}=XYl5D0bNt@y%wF)YHKCn z@$7@HOj2yVs+9$Ij`eVTqzlmLdUJf_$>nIjsVtK%4=64Jmd5hN@8ttrZWf(;Sw~V- z64%0jPX4RT@zCNZ{Xn)WkQ=Odc87$Ijg1G_O5#2ow{^HT`T5;SkE?$aSO3F1B;bf4 zI5Bre2i@zOu(myxg*d8ZnPgn1y_c_n&z+;r@EBJepe1(Qs7p~$=0+|on617Rhdu~X zGBGdQ1oA%I=YQV1v2hakKCNhZMY?*|qH=ac_GEaK@MOe$9hQG6XCHq=k9E60b|)k8 zcnSki{qd`p_(6cuoAi3=zX)S-w4V*H*=*UbBwHCH9A4)Tq$Q%IQ6Dt1{VJh3@1BjL z$1G?K8qqDgk=Pgd+Mq;nsODfkEaS|EqPK5$fB%78jKh%cu0@}fE~ES#g{o;4A z($q-M`@au@^{4z>b&r2%nw{Ol1d9_K&5&%rb&Q9WV9R?lkHh8du(tIr1NfV&#MP_)2d{7tQ)s!i|kzx{W|dU$1OXm$f9nKg!(%|D80E7#JYaw)6RUn1kCs|b|}uVJpWK7m_1eIo4Y#P zBq-U!wq!m*#)f`V8#Hw{xaw<2AFXwWs0QWn>DS0!o1D`nBlkF2oH+*YJk$V0)NW}p z72th`N(*h~DvxA!8GEq4@RuFTSCQ(4Iq|#FE%z>^I+z1@Ekf_={$e41RmxvL znl8rQmj;0d(Q~fq^}xV`AFmZMJ$4}vt+iNFmZs1kH)O>v(Z7=CL*On6B-bNht=o0d z^dsOYl|xMhAOT>zzSP$5$LTAD)4&)bgw(%mge6A-t<(Jl>gF6boM7`gYQ*mzxdMSn z4X<_d>rm_?!0*M8C6wUvvC4qS95MwyX~pL|bvu?Axr1-)Xms)NsdhI%}{|H zs2aHiCBTb%i<WB4FRA#qd8EN4zJRoQGt9|USImfn0S z`D?+6RQXcM$X2(t!QWmfQ53u{7x^F%K;QQUH{f$^RkPKCb|4w!HWraC=1)Zoq5sq) zk4OLgXkwQc+(-rs+Ej+t7boCTg2Q;j55VKsCpDu+a2Er#jVMs-cBiBQ-Mc>cuJZF& zMdC)DwwX6rt1ZH+h+*g-b?2Sl+CZzWnfI{Ch82VLFI*B%9_Lahj~d@Q@VxWgawKT7 z$LWup(qe_-_v_nvU8gC>*Z}vm-?0o!#LHj)ZHiftqHgmpVv-1`IfO(RTwNu?IyT(9 zlq|{K-MfM`cVlqOih_S(e31E~FeHMkNP~6qsk?8+!e+i;AOtxP4FJ-eM;O@UmYM8f zLdS;nbH87MZ2#zOS%P2V3%VZ2GC71?aQG+~I4lVq|weMXLn;*(HF>GZ;}^&h}ho?%px@ z)V$e+yr931B@?KDXA?o6FlD+37lUzZEou$^!-}41cq`kkOibHns0jlA@KD(GwQ8kK zSnYBW=Hrac!L^HySS@-4@dD!jfE_?`@xh1#eSyxOW!l7+$JMW1s|_{w=Oy1_49S$( zljdpYer|bSQg?oedlnCO-;%jjU51{`S$E$&_=W2h0%q}Nd*1|~lq~mDFCR~A;v9T? z?3IE~!^`~s`z}=?uubH;;RP=&!%I4TKO-LZLgwtVpnJkwa48x^Z(ro`_>e?SNYA_F zcYa*3kU=_R1cPIxJC@7iBM40ZRm^YWHy}FQ^m{Ls8C(3eT~r>IaLB&-vQR((J?j{K z930^1TYGB>bCYJdBn@I$A%zshtRwZ$2fP?OE~+`SNyI ziaP&mNR_ZUPCRkV7gAc2mWJ0Cry1RN;IwNAm({!b)`lZ4$6ToQ62PUw78;sY+lD8p zNcr^wPYghWwjBU}-;?sUi8&)hzg|eUGgIaiz<~`P)2tfAyWn zIGJF&ZFy{AG7#W7@v&gyZiTu9zPl+nI_#0>r~TI(%*09H`tYVUs!UN{9c_Pbgm!v)uK@pDI45{T2L`{- zo2H?Wn!2W43xkv#ThJhES(ljmWo_pq9uaKI9VY-v#{qQbz35Ymj9*L39M9{t3@yF# zz4eDjU=3@3cBH8AQh)LK>5aDl1Qh$O-E4}tELt=F+t-QIVRg3k!TnPE<;*{h2VMUK zj(+gql2xUuznErr}+k_z!Kgc8;#y!ADFAQ(y?? z9Hh9}0p5UwRN%+=$xi6m$+uV36tdlazy;&oVl#MinI@*Edjtb2TF!x<;h$pHg>noA1qDm8p6kflqI}59S+B9QM4Xr$&y7 z14V?RJmbYTP%ff!@ZfNyOwV>Qh2qN>TrbG5$bw)P1!BZTDfN5y)j5 zMp3ir+o>+<}$ATEXBTsL>Ra)d?jpRVb% zdV;1uv;8e>DN!7)O3%xB70j`1&jDT^ep!p0ZS;9`n15oUF#F%`Im7N(wZVlAy6{C8 z2bjT21(WPEZFtTh+M{yIXW|0LLNIBsG-i8Nqct#@a<}qW40<`pupY`^0WUBgk8bc& z%bX2(xNrLZzxrdQJn~kp){!$rSIlwLRN$w8Q>Q>BlucF>} zDVz(pbHi#H1N@ZpLU~@I{$i87Iv5B)-Y=i39e1bNG`ZdA0J|Uleu`xYHw@b7>8G3_ z*Ag?I2eKL|FQ40tO9dW+_4_g?VEX3sJkP^Z+A{L+06YLRaACfI_-twh3k5MGb!oQu zuLFyX`6EzUuzJiWz>Bi4Y%qj$TK9FK`a)lt-n$|tVtcZaf9z4r!x(jmCYOWu+6Rm-kL%|0`z+Y8)M=+}IFu$~3>*NQ3=`$9NgmU6zo`sH|i zJ_|USy%^A)3`Ua+8V%mL-~j-MwM|{2DBFgS@rT+!(T~RNo58BRHTfL+47~p(0^doD zEU+Z%1< zT$h4x0PyF`Rz-a|f_-k*=H*EHCw}o|U(CYR5Fitp$FR3gSR}r$qt*Sk%0B{FDH*dc z9&FCUCbrM(znGkth6%uHKz8s#+jH#Qr6E79h1HFGFwE4jVGI&Q*lG8hU+kJNEo! zqtEqhRXVsYQae;qW5FzB0t%pwI@4LTP{FZeNUj#d{BsKcTG9`k_nJp`oJv1mLS&DY)O{R3*e zc?LIPdO-yvTl!B_qqz`M2lWQmS^#u5nC`+Z)U(Ik007RrA4f4P~SlE1U2j%Mh4uu(7IicK9Q6$-_r~ zJQ-Ui+p>dmgPnHsW<62n#gZ!=Rr{cy9_FFOjw{cblDwH2hWh-s+ zV)qz-Li}pmCK@y;Ck6`M@InA&M6;*8{+N)Co%4T4nW@`}2)p9bu80j^Jrky;mx5su zSwC1*v4s(V2#6mi-U`FH=a1(X0j?JaH*Pxo73;6!PT_wSo5e2DPF40zWKIXB6`Tw9 znOF}e|M2wS3$$hY`_CrMjK1^TyKJ8R)3RTWcPMuQpHAkw>D)EQYn82@%Kq?r`E)OH z*JCVwWNZlSKje!`a*P7+O3CTe_LK^vlXUm47Dij6xAxj|uQ5`Kkz0%6p1x_y zPoC9aA?7d1>rIq0vgt0Dw;6d>zoxT)x;6T?ViSB;C%;tAw@@zXyxIsp0Bb_$73GpL z7RpjokKr?e&f|U*B78ZsVN@RmNx#x+vEl)v@sF|0>v>(qCt^*5CBN<{Bvk!&R*90Nn2VD!>h-Ruylre=SoONw z!aY{X?23QG>#2bzFLd5wRnfA@M&qshVUg2p@IgmX24tAtK@a=vik+_aQiXGXe?^&W z-o=F*fNT2N%D}yCWlpA*hy0hTzB_u0km8p!r~oIw*7{F5{xZG=7wz$ZNe^|;0GZ$t z&a$GML?eIhG_O@@p@)D{-awAJ*vivFep5FH6`0LHLf`djFT6C}di^uDX1fYYCbLuE z)PA|=8vFoez;v1=BbL8P9=c(*nDKU4IL~(@-j3cH~Z6x#lzY-hvpc z;+HM@cPTKo;PKX9l5@SLY99(d5Xou|ki?$2gLh4YwP!B(Wr73s)FlZJ!`u1ABPVxg ztjn~G%|Ez=x?-<~vu3k~*DHxDH~m){nL&*?!K8>S1=s;(hTG5Dtl$t6ckeeyTvJ8a zQta)Wyo}CGLeCG_;vy$p=@DErm>cDbFGMl_{=p#dB1GUCYiA@oWTiB}IVLGB!zs*!U4 zG!bypUW-b9yVSt7dtb|9gVD@At%!N1JfyPTfj3txH@lu*mAC4)pLtX6R{A{3TSasO8AM@Dt$Ug7`4)O*KM z{r>;s&x1k|T0(Xxg=EXh3>jr*>m?+6Z%#!~8Iiq1HYt0a63PrAdxk^yUgvzT>v(@| zzu&F?>GgVE*Yz0p$GjfT=ZT%NIKIQ2sh)co^tN+cBvWHaFFi_;v?=${qI6ls+f7N9 z$~Fx9tmA&+Kj&6960=<;)3E4^ZcSx3l76o{KDf?CUdB=Qp-AVu+h2j6;55?=IjPXo zdqQQLIqa_jOHD1%Q_qgo^`ks!*qG5x6u5O7Z_!%PGku$Fe|XkjvgfkSe-1Ut)|*B_UGx7Y zb@sQV!!hr2?Ns{w+T(4nT$Cp&AEX&E}i+$As6V4n?$tlYP6Rn z)NvHWFS2{v+T2^QKs8+;5+w2So{*#5ziS>G@Y{K~;r-^X?O^}fJY&_ykffavezyzz zlm2ev`R|5Ro*xVF?x2mKF{6ve!!VvY7}%*<1|;C*Qkf(E;H#yL+szXhSvKI)wrp^@^M+`fWv(qsgeZs#Mf(quga1zPK+$BRO)u z+un-IbKYgS@)4_~XvQ7(%3lil$`AAL`n~C*;n)(gLdnQ3;q zV^zuE{To=)S0tFPhy0JDVVf(-!n`>SOh&ok;nTzZGkRd`W_%!MYoLs#km~jc@2(Y` zKe%Q!cGE1Z7s=H{jh(l!uoKLsxLEeY->UYLjZJDxs#JkW1yfH>Gs~UxWXHV$<-zRAX1Y)#$#Z5B#t&0*<^l7lrcMI_3;gO~`&-+Qf!fv%Aeu^D2_VxI&G{P!-%;hzgf~V^8pU zKa{$b45xc{&J_NpJyW20p<-2z=3(E`{Lb|nw~hVaZ($vSKDM4g8m8k72731jx;2aZ z?#D2VF?B0AlwGZN+1*)XytY6rB`Itd7&hzv4B`O(xoXAel>bwpap<>2mDO!1veY@e zOtPt-df{&?PTk;MrTcFari+RUdtH*zl<)pACit~~N|UP4h~^#5eyv&XRPWW@v;r24yiMoUSS$ol4n zL>h1OxxB_aFAt1y=Pcu&%DJ{sf52m3Hl8Cj-@E%Xm9vk;^@*{}tG*Lrr%tX^2N=rZ zx0hE<VDFWrBw(M??KwTHTfWo8NN%N#>ciM=Mx3gJosgvT`r%}T>xia3 zt#sKg{3HiYm z@jN;@m}0~-UNoJtv2hvc)3F)eYk@=dU_dROT*z(sIjj`}k-d3PT@}SWNViBW=-oRw z;nhnllVCr1xj%b+L2|C4y}y4^*zM_kz2~6KgKh+MiWZ_W{!%^vg~OTaN$oSE|37|Y zA_eW@OhuUV3NsW*JR6B+PTCLsF6862i%#tC!>QWXNrL#P!7LnUdG!_Qz1DHTb@^ay zW37$~?_)$Oc9tbzIPoi!dOR~Z^}2F{#AlU_u3V7iqE8>0YcvML7Z!GvDsDYd4Sb0> zzunBjh!Ulh*8QGYsH|bb7RFvCzq#t}az!S^qzeN^gT{&tH43_ELPz?kK)G}nU=4Pl zD=H+_Bi-tM%YW)-`ulC9l?EY{yI~KN^&}Xgcs~1-ui!kxNPesc?`(MZ%-==Wv2CO} z^JOkC!F(%*1%@%4TU9OQ@eRX39}`Qn zchM4cDLXivpN(JjULbw4;LFA%l-W&Od5K{xpIy}T{{9^XnoaAbZgU|)%+7m`+h}JJ znLBHpM9yiqS&yEWNc~o7}N3B=Y(fylzzxobIO+MfAVPgR#Xl4=ie|{D6@mVkK z&8C*Qi431B@-M4@p$J~MK~Jq6?2+3od=`VAK!Tp=Ds6GDBdd%WW0Cx(fS#pXA*nMFub78a`**O@#l~-)}rpnx*RS>YN}IT&U;reL=(F%0?w& z;p3gXd?G~=Cey-r=3HIMokv9IJ2j8wPBlu~EZPS&m}jLH%wC1L%IYW6x&%!tiO{}q z!eAIH=8l}7_tCks{hqIv*Qt;}$E9jsKcT)!AgYQ6JZ|YxT|Z6sJK*;)J_*%@MLq(P zxjyBP*3A9G>`zyI7_|)MX2!M_zbLY{Lw}3vgM>{+av+ zUX|AC$Da1CLXbxF-uI_R-ipzO?KmW&Y!ZKarSwa=rIXzexd-?rJc97;rRtsaUU4sj z7CT0TYSum2EdKIp{xh-II5YD3uLm(DzMznNwpuV26p9%cnl1-pOJc}6P6E^feW~qR2=j=1L4O>9 zE)X3akjf_b85F)G5d(do&woX1syFLE=kZJVtgGG8iqH}rPb#1kT~N6YR2gYMdt75k z$pWjJQ_@{v=F+|XBU=i2+<`Az_`O5wR|cS_%`Dv@kggt$jXP*VP4mKdCOcmQ0WnDEj6aVTT= zD|?W+xG(n1tO_29P?I(s%)K7#HtWP=Za2EOt9N_KyE^(d5y*87mzCiOo7puIDxkBMvX?q(18&R*X2pAKD z)#F@TTuU%1e70f+exX~{m!j7$H%#+1bm}T zolVxtzeWjqL=kHEvI4n=hlBA`Rzh&RdqAbyl92#W^yTX5rA%S>k5%F{M4vJur>V{G ztWrmdN%U$3)9j4MX9$`7zPutq>3lH8T~@ba8~ViWclv$Mm1j99$Q-YaY44VWLy-}K z8{x%k)}8u7L)kImF369M%sh*~gnSrqZol$0 zQSCJpOIstJdB8mzM+}Grh6|>B0LIiP!NT}Aw|mCNoZt8C4aP5fL0U7Zi4OPURE+nZ zu{fw8Kc>ze7B+!daRPhz(svm!S2LqtHeG&VQ=w+}wev9zw1&H^B)hFM5%st0o{L;T z9y{&gXQ$2_S=)J>834HTEiT+~c=N=(J^d@xxxhf(yY?Z;VRF=;`#)#a=I)U#bTJoA zb&w(YmvIdxmBs-k!$)IQ609=H1%>1`Zqu9uuZBK-!>%NuqI=2e9V>~M5{LAb^wUgJ zj}!xrhRGu`WRM=$P!HDXGmISxq)?|_0qKi~+qG666g(5`88PB!%nB`cdVAS)uJ@ln zo(`kgKG3{lq3hlU_oIfgNy&ibb42HlhN>ke7jSko^h`ki4+iLB?M)*`+SugCfNbr{ zD}*&$(d9^%D^vm0-Hg%yB=KFA z+VkItD@U1;$1iNM_-@z^Y%PAX_EkSe^#0pWYZC(5<8ToH?>PMXSQQLS@Ye2}bijX_ z(o&JbM<^L#QQiH+M7wHhInftg*0SjTObs1I&+7h}{0|Mx{u3?Kh9l4-5b4I8v~8uS zfJV`It0Q8}1V^E3_o43SXVzQ6goP%C!;wYHgBNEzK;eP-^j|LVcB~QzKBjatwSo2V z=0r=r_PNK0AjkL|S-)$U$n@m3p(rH`5c_NDZ~F4p>@U*t%OdXDC;boaXB_VL@kiJ0AuJTS zXG?xbA>cVU3*dIU+VxJNZD&6nkj`n8{b&zQtPKjh{D-lp_#XNA%tgWu`vJakLEXvB zg%2V$9wC{t_*Bh$2H#k|bPm>c>~s|+n<0Q{OmjHsBwcc}frtu@7- zj%;mfytveJFqDof{b-1P8Zu{{%t~^#oZ8ItTs8_u7-p!&@kcI|1TC(QO*aThX1Z`X zjse?mERW02k8zvzf@?lfPvGnxx!q18g^H-NH%@AAWCJrQVXp&$!=rk~E zX~#iwHJ5q<+n*!cba_mW1xW+583N2E6>+|Jy>ys!%KKSsZnmk-PTO7m5NJh405#Qvh{UcIKIL-|VYQ zfYqpQl2p1;5F~fi#^Z)t>9E2h=runddiw|X2A#Ro+5*x5H_Gu#KgWku$TZKgVy21$ zFS<5+Z2%Em9Y-*IX3N)ALEQON(UN*=te8*z%(^9bi)skGSmSKN)?fqPV^R)8DukWr zA#OjHmX9uk^4J@^ISJ#jc-~>p7Bl#j2^_JEyJS5N=KyFv0cQ58CzkCdaXzE~gDS&E zZAm+YrLhAS)PMbb%N zDiioG9^;RkwMpxD0|^=PZW7{?&z8zc1{Q5rOjg|L4~9&N z`Tp22^pi78lf^wb7bNpzhULnY=T`!WT6^v$jHHiApL}+O)3r-m@6bQRDs{`-urR(S zS8C3;rdphS*)(3yr^q4dBn_JE*WgAM-BaxJxwA=+5lvYhuhh;$d9;iI+9H&$t}p3t zAYh!Vre^HNmRAo85j9F`{lBM~cn?>V+8rt+%Yu%%>-h1SidBAKg=kHi*1O@gQ67&8 zPj9<*%mkicXb;6BX}^2;r&p??$*AMVFfFikC%f#B@B9>teagp%(4_qcQgQ2`2BNHzl zVISU{)n!Aku%mgfV}(X1fJCWP;%hf2TG_by0GpZ^S&MsW_fnHP2f)3gVYBu_m8R3+ zzfb(|4m*H|2>ktpVST7?;a?_AE171yY!F$R1JgV0$Z#An+9r}e<3;}T*LcIxV8k!% zWOpd?hJL6_GHUKSlun|O?wsn)kf57H=W8@W97*DYSw6Py4oM@9qe-X`$F9Pxl$KzG z`Nv?7`E3KST)_Fa*~z}WvugjzqjMC|cv0@GOUy^4$UriGC7Pl5EGif|A%*K+vP;rt zL6AqE>zevi6W2MCz6T@s-ylkqWv;V1qFl6y=ft09%6*)t9ySw}NZ=-yj8Kqq?(q63 zy)oXcD~cq+4@h=9ZS4<95fkO6w?&sz!kR}^NUrWReBCckiV?98ErH}rz3cOe<>Y6R zInRd4W#?fGp!UEKBYdMbtBp=LoC`tZ(M(KuGG+*CvhbGv8DmJxqi_U2=QsukFIFpj zZ8MgpJsE%|lOdU(T?;?Pg5$&TzW(ab6YXU32%8KlT-vEJE92!lFo4~!vCTU2HufAu z@BfWY{Cp`~_Sa6G8^9;c$J4FzFXdGlmUfzn{8wx6*(T)|k>Z&je-xW@004@6`MPxW z_fjqqq;qHI#{!!b3Iga&3$s{Y7f{!@ualA2CV+#CGLCZh5^!2Fm@>;O1)Gf&zv3G- zEJieF_!!>kf$2w@zZanq%*}B@5Ri;S*=#)hSuJ2qjrzuj0^gO#mfo*DphEnkf|W(W zu7%G6;W=*Z#s2&G#N03}>J)%v4wK~SB0d<%$V3XSN2Sr~sDeWCbub$jB?PJ_{>LWs z1Y)v;(U*}05Mm}-CA=~O08HjUa!H4=cW-(sNfVjS>T}A-SA4JF0VE>2eHgw@uqCsn zt#^y<%Z-x-gOSO%h)CzXA5%*K$ZDBJIPW-KCj|<-X@hU#6f`_DZ+khD!P**N)hxa= zX}2D2{=7yYT_l&g$effjL$cy5YX9(%VaVBFxD~tv2~yZOtlNop7N{Jm@CI)cBcW?! zI8rpr?&eq`3S!F76W2s}Y)qnW2IeH=At51%VvlskUB_D}8JQ#z!NNz@Z-7<*%APYF z`PKQ81mN$ASj{v$VsejO9=$4$?p4%J_#GarjF7#SzgKPfn9k)oO`1km*9DMvAgC|4 zZwif@#=b`bW<)We0#Ql#5bB!);oLs>=E8R1TgD{uJh6ZC2M>K^`yD zm;r;{P&6PcU~lC~78h*->Y3l#n6t%J8T+0FxdaOsJBCuSAlaSyU0J!sjF9w3H}=AP zr6i?y2p38bO{D(B2=o#1B%j`HhWs2kq(YuAB&A>L<2(&%9q8&h7T14A_J)h@!zt>3 zF*7B!mQs^Y?du&piY2EfraNfM8bYpkz@=;Xsb2rA5pWHwbqb5NU0jrGGN0HnWm;Z4 z%*bTys)kRw$kN6%fN33~PB;_8>0oNiG@LyUt#+$B`m;L_EwMm~-Fw z-^Rx*At?dH=wdroU<=irhtW~fDyywz$TO=e#$#9=1hV$+#@NqLrh8)D(NmleOB)@pE*Fq7_y0RGRL3tz!9K7Ri#OF5Om;e z|C7m{1bhu;!m!x>v!4*$_X3JH{znHMVX@`S)8Gdp@UBboaT6hEVYN_Cc(6VJ;OlD$ z!pv~Fch|t=EQ-aJQ@2T|in|l;+2?IfZh8W4sZpNEk+|`HPaEN=kV!Y(SUwVG7=kDi z&F#V~D6~f1u$hfjKDhX2lex);e&^clNd(+{--*T^C?k`?xfl`4-w73BA;1z2XZt$J zSB@XW(8!t7>)VNjAS{_@azkzI1wmL{^#*UiWnECySBELb_zAR{`_4I1=sTLmowhF# zxW8@n`#-AhbrnLl2h$rH?!oCeA}t=`D&a69aEOA$Q<+48OGd zeTrJ7s}IP)L$=1U7n~Z8&)wmMX)(sMyyEVn$8eCwWni$FgHF1Zzj%cDL~dMn32_u; z>BE4F>h|OBb_C7O^r`Lfn*4^odpTL`Wg9>#w>o3eW1#l)btyJKX1DAqj)@EvDIXlI z%{d3`Jvm>Qj!{bkLC`s(S@{PD#Hshgt~Kb55N0nQB@Mm>c(_srgY~taIA(;+TsAAK zcu!C+LwNOhR2SJ6>{u~63nTK3@YHI$m5jH}s<)gdf=o`sN5E_?%n9SGSTULOX zyWaUBuo$19$oCj(oqgkXpNmLdk|^mr6ppf6ZXBoX#k~uk+6&)>Bf|n-OFw4Bo&kl| z_GACz<&lfrl84J&r@0VBM!SPC&J0TD$Ww}tki9}aWnlEci{}*4ez@gnNZ}+BK@i}P z=P|a49US zk^926?L9x7q2tv2Jw0yU>9h{S=DAe3R4#Xpl7@wwBAWfx8hp@W6GH;wlGCz51BDhY zWRbbE{pXCjAlUAPwk6fyEGp!~@+W_C=p^oUbtys5F0sAs?t>q2g2Xd1nI_&!_>~$f zg#=fJyImxh$*4pecfiitUyj<_zYam}nC4uTh=`RyFt0ui ze7|tr^|b{!HTXI~B<90CFC`V)Ps*cz@M&9~nc|=V2Ku_PJNOns4Xfj8CJd&;(7%K% z@jS3;?g)t_quvZ^6$Bmga@tNc#lJ`!(l>(Jmee>hD{~%MfDIm79 zAPCc+p5Kj`U!#F?w)u-I8T#}Qf}Yo`$c~@sGx#2;OcH!38wCFU{4M0YA@o$w z16Fnnsz{+}EV`He2~wjsBB$O9YDM45@V5qDI?aW>)tvC)fXnbOIoztKb+~7_oQNxQ zjD;xhDIkgg7=3m3UnK&Zlb`%J3zckCR#V7M^xhT}AMl~_m{@T{Wk15fOcOiGa3|BDnA%6%<8u z`UMi;DF#3}z=~yyX(BikO1firk-*5q3ljqjpZ3Caa^OOjDU~l&-2fsKCvs$lrc1kI zTQ&-Fxo0Lhr`f@I-Z!safN}WzWfc7_UpSzF5u)T_f_W2$R~tAS^m8Hv8b5{%LojQj z(l31(kmSms*Cko3Bg6>uOAGn4!$b)COiW#B(D#p=MtI6SV2-3o5nb(^k=Z0hjLbi( z7hOGm&Yr0Ym>UpQX@18v+$}Ds)ePqG=Wm(1x}TGQ)=V2dN6<3w(BRYy_)Kt+N`v4W z_-2Wfo3S4SfvTwgxg^@M$G*2ffcI_C#Kz8^0ut&vnMuf;CR$uhKpDOna#5&#a1O;* zr{$y#tg)anDS~r1yV+`frM<(Or}F*m=h2uyieQqT!H#`?bE-h0pAdI5}EC1 zV`3h=j=DvtjwVvJ3&KH4hAzIyU3Lwsee=cz0hQ)pWUNpU()>3jg71KvUC=z_*(Ud0kC(B{628QKj9!Ui^LsT=Rq=rNPR+%GdMQR0z^{m!ZZW1#EH%NoUbkGM@d*R-y z!R*wLA0dHg_AJ*_)=%OCU&%cJk|X-5J9Ay+j=VAwy^ zIDk9{E?C$Rx?k@-2L!>?FH#Uv5<$eV9+J<=bbD1H0m@*b#VS4xg`8NZBxiv#u+pzT z0T2I+u)xx6F6fjV_`=VrzU{A>Utun1^35@v6_~^h%zT1WsQyPf67eM-M4S_nQeSyN zZH37|WJt`?dpcw}S~$ER?=mScV*c?CBe-;<_n*AILA?K@iS?Aw1I(Q|gNW69+6yA1 z-Xn{6OnUryv zxX<7vnAkOX^M1uK12QDnQWSXsovjD-!vrl^cWeoEK`CvYL{33~`=Bt~+X~5K)O|rJ z<}VWgDc&((cB+U7#m6JbmI{Kp>5x;5RfO=f4G@H+A(ZjHb6Th1SwFpbe-ez+_Yx9} zyfe*Ng@Cz(N*m>MPGKdI7bChNe@tw*hza<&dwS2GPVho9a)Jb%B6Ujo$SNJZxSMeb0#PFi5G?ianAAlEF* zu!S&@mr$w&GnP+$+V17+kd`j`*L(e!*koqdWro-Y?9d_)n!mzb;%K=I6QH{yB9dWc zM!{lZyGUW0kmL<$XD9mu9R<3jf6j{EID!27(otA0x4}fia*8I6*@Hk1Ka#1k_CcM5 zY+5!529hApVvf0qS|t)PACerkV{5Z{+LHy99cY&>3m!Rt5m`Ue| zL?n~YHCD~*ivLl4dv@%EBctQCs8TCg1Nq~0m&m|nD zAH2fJsGG?&8wna1(TZ|5Kg&)OXp#Oo9-K7ay!^JXxxn}ve5#xb^MAVRQILp(K>K7* z1vdIsM#c_;NL7n=W`MW;2qIQzC{QaX?)sa z9N>xS5@(IECQ5!97SNbF#`ose3kacg0;^>ZQ3{^6U%;6Gi;@{3fq9#bvTEKvSf~d{ z;uFBT;Ygaz^_`j52gJafmC&c!f;9+=vV9W0W(J%HiBtOevc2IIpv_oR8voq7!E-X| z88Xeb8axYvEHmgX^}z~sNH$B1-46U#C<-yYcOjo)gQ&&fa!MPcJU__Wh;47}2mY9F)I>Maed8+@5`p_jn21o+@r z5}?vQ6!9|jb|Hma`#w=KSEpQ(R1<|I5$-r{5SLl!O2GEy`FhPsMKpr;bMxDih8DiJ z!YJ~h8P2AjMBqTOI`H+x^w^vH%a$j$(F9L}^ysS`lc5m{&3X>IiP->P@24(;HB6OefLY4e%*x1$3;);1u|jA;zT3p=%d*lXhl~ZN!o283w0gXMM@&Lu5uNu~Jocjl<{ON7)%dvuc0TjT3keEY$ zxrlBU0H=w*>f@`$xeEY|+1eV}Jtc*w0${BwIig6ig}uHV$*P!eAU5tXS6m8!KJG0-RXoKFwv8keF7^3@;vfoS zCAv4U4xxpQ4;EgHYQ5ibzI#q@n%@?L{E` zaL?-ZabR%xZr(E^K0OT4GfuYp>nQ-DJU1cd0nMP7S?2Wb{~HEv$%*U9{sgAyC)WZ@ zm4S{w4Ubta4%!X!&Qd}jZ~(^{2<$MQM0V=sQpl)N$X5H$)Ssb7zAZa24E13v@H7BO zCnQpiWkZJ?ih;!UkQucMerFM=uzlHR*aTAx9e^utH~!ciwICBs-_=OSKM-nQrv;9=<^>%}GsIY>jC?uA$j(Qp}{N9*Q@ z3E2L|Fw>w90CIP2?K&1iQh4!P&=&Vbt@_sqUIKp-oqp;!1k&L-REmD;fR7L8p(BAm zQ^)P}`j^{A8b!2nm@Ewt08t_roog{)y!9o9c+zk z0)+rK;GlNtkCT856f`JxS7Lxwu-q84Zns(Me=A2=Fh}((8sRq=OfE)X^Ni0tlU?bO zDXfMj6wq_RGhjjE6)kU4c)?xKdKY4Ud$0)$1R^C+JYZp>j6PqSVeq#m%VVdA2 zPS&f2>98y@bvA^ovXdR*FpqJ8O?uKxgc4EK38zRv3kP2uuD3y_VA>8mSi? z-@C1dpfR40%@u9H#Y@6LFp`y6b94a}Mh}<RF$`6SH5hT>h!3H zm8kOIpt>f7;r(lCqo5{okh)Tnd{Ls;fcDZt5iR9^d4lh{Gq4#ZlnnKeyH4WJX9aa8 z2%l#*Km9=u6fC%uJVuHpqfRE%oOfE8TKO}|dE_M~LQH~AE8#dgT+)q&H)@yIkyJb> zxX6hZWIr`Ha~Ab5$Mwj{-YY)b)rgS}D1|T!095%)V% zwHNYjK!m=o@R-|N^`Q$V(8GpFKblf8d`Y-X(L|XAia+tDZJlIzQ<%BcZ(1ZM6$$KNC=2P zTR|$Ed`B}11Q~ihkyGTe?>Y%}xhq7pN>Dp>oXFT!OBmJa`LzA_|5L9S63s?1R5){M zRWy(Yn)&~5_3YV&p10}hYOp~Gs)q{Y3Z6+w^w2joI-a2<%3YX5Xo0-`5@Wyg*PjsS zClCA>2r0s=pjIBfzu!Rw!5HEXvSOxi$C-ooa^M~1roCNYuQCsb(7osAXjt+nqCw}{ zpj5a^`2@mLRp}p1_nFFbmuu|fD%tuD^*j! zDvm%Xkwvnb4mXc2BGlC=;db@;M4*H7wA@s+Y6uuS9jVV#D;AlN6$e>J?Q0~Xb(-Gr zk<)XKSOos8T{|Ha0d@uvWkvAr%JP&tNOM1yCvA4<8Oln*1;I%6D)cfkyGa6N!J*=W zJZRc<>@L_^WOQFQzh7{c9afSfVaTx&%TAxXOG25f# z{{WMWhY^vDIko&WOtM)DIui{R)V#IXWi~HzFM6B0D~ISR;INcWQ&Dbp|Fdoze{!nO|Ny??J|L{2ZZI2QKPRLO2!JRb-8Wog$Up56G3u z`eDoIdr!??FHyrslWSI>FM$1ojfcKQ)QCzR57$u}MF&jxBNfmti&cl9;AYtjW8&!3 zsT6biqDek2Yav57cNTfy)6r&F=IlY}07R0RT}i(EmJo$yyIw0>sqyn_jNH%$J!d-Y zub`UJuU3c-0wH{usC_Y-bX7km&?)_vKQc#yyl)Nd63)M7 zh@lVxh)9d=3XqD0=#g*vIUSn!0QLsz0&v%es{nRXCU^3O-SE|H^-qI9s`l&6oHWP- z0frfKD_s}lr`P3w%5vPGh>m*)e#zkPIV7n(qobcT)#e(G>J|{lYfs!-bPT6ni`XP2 zWo9JT_)4Gr zU9J__Aawl_TTG>H@mM+mR~ans^;|GB4^18gdO&~uHmE{N96?uGvYIs`weX3HJAoqC zwcR$QOff8EmzybyH(7t;xFgJAtvtZ=t&I~AwNOed%%>Hi-HucPRBiGTUGA1n57D{E z#RM36?*>y<&|{iwF%=N_$QoUXr$W^C+NBe=OkC<+H(;aDj9KsP%xC}_LH|h9;bWA@ zoC7Ho^-_>ZPb6;Z$(Rsm6ONSR83!1Qn(TYM1ZrcsW}o?HcV8L}CKP-AzKToXvSE|S zq~%VP{??OY0DGZa1rDu?E?t5(BB0TuC`FVc3}(u#%cT%26brUh|3UltoBHQaY*m_c z53D!)%ss)?Zc;>tYAla!Pp9zkC^$u^hXxI<-N3hhp6%0P|8$>F?*}z4kG8+}%PjgL zdJ;tGFg4b&?1@%?qE~d4P$W$nCrz(@V`P#qNHKPQU3ir+)$joL=KY{nE~{qOu`4Fv zv%KU1rm5XKrZzh16>9fZEyEdwJi*PxWr`3~8YnV6H6M)Qru{GJ> zeQs$dz3M`!*+T-x_dmyl6{NB~x-LzAM8j&S8K`yP&tYnt8Xm#WsJhR%Zbfx4 z`{{Fn(~N=T()*);3EfW zPg{o+YSrP@XQw1SyBUn8fYZD+P1%$A2cHFg)^%-K z&EN~y_?rQ1n9QzpNtfREDDcXO%M_sLycv$BIUcOU>`+9n?}*^JLLY4ayjHEnU*s8y zTWVYsgdw0}uHlhw{Mmo5z;kE@YjLTtKV>w?0WI_z&1)KWsf;?oM@N!e+g=Ngey;6! zMyPn^M>kqRT^U92V18dFy&%pNd?T1w0yItxaJSFbVioarP6 zxuMKjSAe^p%9e$#3p^-rPs!bvSpv+50NMgw@fJ>uBCo+0B}6R-JCk^Q3zXw)dpBUf z)g0Lf@V>l)0|-gG>vo~1N#L>xpiaUxN4qv)P?|$ARMY5NkR=DlUsTmBs=UXFGC!7BSrrXY& zCxCadWE|S_V#6-oe-0(L71TP`fp@Y6;^trS!ai?F*c=K=Qr&>_TEEJ)+vi*F`))A< zMeR>~aoN9xr9mBnc;?e6EeuA%l4^O3OVX6X9e!NIy@Kh=Sw*y}Vy1TNe?H*F9<)@Y z=S$$SeA?eE`o2FX09@WVeuTXX$;>Hk_>;F%H`G7<@-0Xp96{M!{L>-{zJcJVh3 z$*ID4+Nk>wkmTe?Tr_F3f`Tv448SSn z*Y7<@d_h<>ak%2M7W}?h*C44BE~`_uylYPC`AKtf8ez7@G2=6{uyT+)tL8wK zYiq;FfMB5O)vH8@ZC9iYs6e?I`;Z?Sz^Ip}9PLBoy0d#{#Q~R1X$f&<-8v+`GRb+g z5V{>1V09Zpe_&49$x-&zcDTzF@W2>JJ!a0n0dOKVG(sNzHM-}y%^)~0s=V}%IIf;k za@D>+h>{I1+?m6#&p4|a0WDNszYtsRTua_8%Rxe8rdhT5M=0jwng8I4`gOO9t9Aqz zf_TJeMS`IkR#F4hjb%4pa*BEZ=RHxs82~Z@r;FZSG_t~)jX{h_G;m}O z2H5>JtXXHW1^4iTsjZ4^P+tS}NkqO@#e2)<(w^+kGJIc0D&BAbwc+OX|S67|Fgi}N#3Xn(h)VDSd7$MqPJBrH| zYhNYwC6)c{_b5My5|7ubI-52>hu;?j0UQ+eE#9@K0QdPVRJjY4ljHC2yyZt6Wo^`} zpWb;>|EW^P%dp5FNEXBFViUoRsZA$mybba!$SJX;$q7&|EUazy^=NHCscQ8L&kD^I ztL-l((zOUbmPcOA{^zxJo0N~p*Tvuj*Db*3AuABAa4|aME!nS(7kRg(Hl+QL=~GD3 zCr>SPrIEhO*oxW9qreCy67<+Fp*kXsKg; z7Qz3~RPZ_sknadvYkgUo2cAOnxmR>JvFl!x-wQCp2teR4#g84%uC8)CrAKnfgk6$< zyyp+@EeA56A5$P0@aT=y*5mx4jZS+ow863OrE;_4B`Z;qfJAOM%yT6(QGWEAm z#yOssP>R0^j=Hu)pty0x_BUUQl?1-ioefQapI{9`6i4=$SrA0Z^6h4F@9RJ3fy9OJ z10_KJT!-5oZ>8L=qGFbQ#6AX|iF${`k)Oy&lr1a%2xLZZmHW^Y>*aZbPuu7%lCiI> z(ODt*5`>;jWv%9Zo$!kJycWaX?@Lt6kKN5{B<5YcR5C#u)X2DGZRq%7V8F}qn5z?f4<*v ziw%7xl=&)8^)o1){$@)A?OLL=@<=4 zwnP)#_@BigCd>mzmG|f#VARBWr+yJ%;qj%&0jOUdsc0T4z&RNUb2lkdy{Go-SX3S@ zw+oZ4EVIbU9go$utMKvY7hCrn9@KlrhHL*WZvFKz?xl{vMI`N^!*cuL%b-GU=kc~9 z3a?NPyP^`+!^0EHoG+v*E*|Zg++b!ridPYsz1Wf8-Th)JY4_1Mn!b_gw;bqzzJJKg zdo(b3(C$i8eb(%o3~=4Q7{F+DUF@*XMz|XITw3SO_~3G!AgPI zNPg_rm#0ESty3phPBCZ}A*<*0z*xZEp-PZ zgKTgk(p~M)ILg0DBEwgsxC#2rU^W?J@s9ziy3G_QxPFcy`$=l3(=Hn(69{#SpV9)H zeZtGuSX}`BE0a&6@1yaOLM;N*P}UG+JkI1}0*PKAFmIN?yxyJ)4T?!HUsn3HCrSiF zbhIqA&)#L5?w>n?HhW`jN5s3yOYm6J=8<-wUuo733Ze-%=fsvuae$*oUvL1LO_AyEO>XB2s_SndRx|F_9nJ_@z3G39Jo%){@Oo0Y**LI zvh<{+Ec>VtkDpMnb`0tcL@AOE&}Df|ld=xHFLCS5r#qT!TWa!aPKEaFL~`lD#opIn zY;+PIUza^TfXwcFXM?V^QEw=@vg$(ElOFQ=`7$j zRR)nM{bwCDd-%|xn*Sk1wEh<3%HfXH2D5%5z1@+!vPpwU>CW?adf07o?28>sXx`w9ocP*B>n-;HKJ762Ym%!!|&{u z;T21Nd)~0nOfPfu{T^FYuF$Zxa3WLBM=mBJ4MRl@waH73eK#SdU4hNes}`r|7rt-W zegUKp&p?!WmUXdF0g=nz9nusR%x(R!moipT?9WeMV1F<+XuY^z^Tpe;ZfPNI(cVcS zt8-rKmBIdZS5=`A=~KMmQn5xCycT|)PO|12?Jgx9aH$jhE4Ej7;`FknU#{{JkrC8@ zLP+0|V`mk5^q79VTH*F8)yyq^=kNBeyO#X0M!v(iZKHa-zQDa~-^I!3T?B9PQ*7@m z198`@T68KlHg5{?`U%}#F_xw2-MnALWPrO5yiH^zB%C!oyPEtJG68@TJ2t{L>KVTI zBXOBe`#*l{YyBQID+Z|*=Bj}!SVN83tqv}i1y8qi{M7EJM~bNl7TBKLm{uwXUOnVx!wnr6S%;FT8jCdPOxgj;WWQ{XgHwC+a?1dYDx3bS(@N zTa~(pGqxTJYBug*bVS8i>g9_2iR1lWU*ZDq&PMpJ29`QTZ%{y{Lg?5eV&K0T4S?|LoiNp z;lehQINb)EqHBMkft)~sR9EI~*rJ2-;49nuR{cCym04`D?QBnXMwS2k$cx55LJ z=ByIp%n-MpK=F*mm#Qv`9w4O^#xpkvWY_4P-$%_&@P?TyCirRIo6}g>O}#I4@7evR zFIgvSZVMj;7)SSZ>fV+(wtp-0<)UeBH{4y`Bhq7mA^~=|o%#*tlBTuv#_TFm{< zF^#l4c`OWM*JGn=>S3hz#n>=Ly53y#|HR#H=#i4#F;w?P zR90V;{g13M2OK`B*L3^`?GCB_<((YU_|1}Z#aCxS+zwaLeH&#=>&kljT^L-v?afWP zY}*&h3cV~g-ikr*U2M*4nc1SA+p{KD`TyGc&WEO!E#94giUomVAt+5jQKar?i>$r zH5Z#?`CM1*A1M9kTBBIpTQM-u@9~>YD7w1c++xbuIAQKDkkPXyHL%~^yKm{(>W$Py7wuYy?*SD%euEG+*4Szog>Bz!guxm63|zF1?)Y%Y%i4#6leEDe z{%hc(|J^gr)wp8Gi>=qZnWL9qm%r%fA@6sbTI>t;#85nN7mm8Lijl_M)K#WoW&gmC ztdIBW^6}fvto}7meP(x{p?^NAsc0A@^08xfE40%XolBD&}NXuVdAXSRXjVw<{ymHY?_uJsl3-)Nau1Tu`W9_Y-kUAsQl@U`676iceLoNW>*hejd>b%Od z_9IL7U;zrE0{2OPSm#IEwnNrQ9*b-3(!Ez1VVRF=x&PfPe0LyEzt${Lkf4SD0X8`O z*8oa*0C|5v1s;RQsYkIhYVlSz*N!~gh!WUCoSa30Bbl*2TBCCD%#e!dP*&^D(WgN1 z*p}shmR8B_y>Ge-62AEyNw~!K$sE#upCyzZb zu$Bkl%R$EM+fUez8@Tfdu>ad~4n%9qOo|3J2OX2p$`N)yX#lv7l*hbZI%= z4(zwH|I7)+BKygq!BKGQjh4qz@;$JE>+(2yo4$wdx8-o^o zjSgP;aA(Hgf$r+t4ZBY_tpB`b*h*f_r$mPz_rCS(2d2@yk85Rn?Lwf@D zV`1C$51MnOuTzb}!m%jq5YUyu=lrrp#WBRP6SBz!aL+9$0(VgDv~Sb~Yxuou3m_mo zWBvoP$!c*CI=N0#QYq(E*Bb$yXln30dE}l5tvvt_1Mnk=f5{%Hqr z!sr2c===D#n(=p6?7dh2gjads=>Xjn9wn@9&=59!SRKGn#-@{Ny`7F!N>FH0Hz_F@ z{E!85D?<3M*8ge5OinS>&KtBmfJuF#dn;Dca3hq;FODRQg4wy;Sjq}11qSoCiNes! z)}k2%`=716^49RqMf>-Aq@iQZn!s2zDvEQ%1 zpaDx4b{#+scwH<}&-ZG3-j13O?^`CQ>2RPhS&+G5X2Je6>J7L1hdO3ekeyAC)Gj-c z8+~7G!he-)Tdv9%8 zzw`m8tn^Ku*9Sx2-)-3oYUY!oU0>%ush1lz2dqip)Jr$KrGni-#ld&?2ry=>nZoG- zv}RaIc5YkIXXdHIuhI~h1K=6RpK;H?^B;?=6ZR=^Q`m998q31Y+(K_u)XF`=*IBQX zi+3Bu68b-$lLpztNk^fO0)06y*ZjX2#CL)q*3y#RKKChsCd0d*qb?4fwb2rTTtlur zd0qSDzxpOEqdiU79UWf7hvkY;ejJ&akuSpV;uyN|<%MRHUF#ZmYd%8;xqT_vdOu-C zzuqY$CGq^Lbzzd=!S#>eGJ(yP|663K5`k|I5q73K64g$SpI$^P=t1A-e{Z`>0k3fzR}fc z#_w6*s=wzKkMZ$H22~r@Vsla$V}`J_77?{Xt(lDI8<3k4&Mirp(XDr)ngDDMljMEj zGcj-@^j{jhmYH=bX8l-$v!b&C=b?RV5IdxcwW;&*1mj$Q_d};y9kyD~ui7L|@W@Jt zRo|t?^M>s!xW>zket3$L-kRUnP+g>ROi2v1 zMA@_zF?I&J_|xMHX8K5J*E;uu8I6(eOHXlw=U*QTSP51dtE&Aq$0T~2q-Ow}o10A5 zeW{uw*DeqCM6dd2N8%wox1PKQs_W=(`|n_Hrd?cDftaL39P_|IVG>B_;j09o|IEK~ zi#^sNvA=;%XIZV+()>q9;3xH$g!UD_Y4D!Yfvq-Mm?zDjy+{9xoAR7?a40`$VK&t> z-g&Cz8F0o3P_qT9x)9F`@86ZuTtEGGyxL^9rdQz7vKW|SLgb+;dh(1xy;;}#lDQ2J znL3dqsgUo#Hey?@ne5j71HW=&LH-Ilzri3KY~pg$Hfzlg;kd*xZo(7j+etjoGv?vc z-Kb`8qVwL~lhEG;Yf2yD5UHKg?t-&QRa5i}!+%QtJNKmEbEY%sFmIbM@ z5I&xdnRM{Bo%lAyZ-+yuj?FJtaq#!8ZU_iDgN!oYGu{M`-+Z@g9i{(rGM$m8h+ z9djyna(GewcIhF=Ju!mtbLr#4(1=zzaGU=+#NJs>;$+n~UQ^j8YYK*g2xlook_QRBBgy_p3FTg?FgaQrBQIZLAsgomP|F0;{h=b5!G6#FM zW^P`UHLe6Ke>a%+CU|Cw=AX~B{v!*~Y_w)M&aBvzMw2Pajf7VF$Fq!)EgY(+g0ntl}guo&2wLZypCB z2+>gMMX!=_SLx6XAvepT;0^)3Rxk?!JGOYupkOz;=1QHC$wNQr$vabBa~v&C=BYMC zuR=(p!?W~>qZK|F(}Dd5n>wJn($Lz&Q<0s-9wpnv8@LM7#z4(R-O>KG}t+9V_#q00kCV@Q(c1_K>mb)G;f(JEg zSnm?rB;S&yn4b@0TU57#M8UfoAQ`XvtaIXmBu5bCzveyONpy!%U=0bDgIeAbt`2wK zX$Zc0w)|jX6+W`Zbk{jEznz;8s2d>1tQZ+8vHr3UBYV7)uh+!W@~6kTPucyy`E+{L z`rG)B`m`J&!sKMsr>j#B4r=i5i<0hhmXHxcuVoL!d9nB4aCrHAerQQrJ{#7!V zubzVo!a+kO@lUrh^U@SMH6;@xUR8}>=38{E{zWXDD-*^bukvca7~ozsBSx`wMeg*w ztep9YdU+D%O58m$Y=!>;P#ovTA9AEK0L-3ImwxH+nc~i`!LR_~Em(5uHjviz=Eq84 ze9et3smJBVaIs=o3!2K(ZZUP#>KniS<58$rhoMQO>wdw45zo8L+5K$>xLDjh(=xF@ zUnv|pwX5Cr@uP-o=a2ZgS`p1Vd#;ynM`421jg^OZ&lqP@TgEni8uP`g)~HZD!@$6 z+NS-7QHgiozVIoLNzwNHPlIrv3HHJvEU{q1@LW~4lz4pip4(MMI|QCbMx%4Dpg6IZ zzOgYZKwoO_$x0VlecrlEEaf3kK*WD5pxQkH=k8;MGmy!qD4UfnZ@3lJ%;UQy4kaUTiay^r&~?QrVgFCIdbzwnSbP) zlx|;lWK5Zy90lW-mK4=fELP{Xp%Q)~u!W&}3wm%>_p4lm*6Uf%H#^cWX9 zM!HQ-R)gh}U;sHuPg=zA3kx!45>^9W6uBDozTbiP+c5Bv*tU~8o^{!N;!i#kbTn3t z?xcDWsw3YTK}M5&Moto%L#Q_$&|lcD^q4UIp)M*udUP3Lv}}y zB0Yz^63`xMk+&cySccTT&l$Y4_LpLO{EU51Kn7m42i{GDYu(ynsyozAWbo{c^qNN% z>}|TN1P!|rmJb8B35u>s$4r{{1#l(6L0T8%Z)Y(j^x zey&nV5eLh_^>`sym7HBzTiA4&`;mImlUri=@!^WsE!nf~j*YnEmk!P^=)tp2MEaMA zv`3a2#ev;fsnaA2pGkvHG^BWF&Wix^>2|V|QBn+=tnd{Ul@EW2Mdugsjga$4T6UvSd<5rp(Q!t}-|R|k z6%+3gR8w%)*hkElEq(&ku>L~RRA3hcN@AL2hnpUQJWI`rla=)srzYCHc*P>JYjS#g zA3A8N6PB7iLVNFC5A`xy{)rqWunazvqnb z0(j}8u|vJB(?;vhaEh zUhyTz@Co~zRX%nKs(sb*&|)m`{U~n+RL5Yd8IYTa!yzwKK-EvZ+%&jhKbO7E(XxJS z6|)hp^*x3s9mhZ5MibK{d)2!X1-}hPK;d&WS(D^sJ=iXA$ML57&d?w&cjw#lD;OQ( zmJm<5HMKIs_kVa;m&2b+7has(fex{pk zPX*VxPt9BaXRT{yF_t87`f);AireI?gNYWB7Y?@BtU{^f)}3Y}Tser;C^CjL2?kCv z;mBp%(0Rqlg(RQhm74DK72aU&qFd|Kn}%@)!(OR5=;ndu`sYU0+ue;0jl@ZhJ?WG! z*ZA1?kzn%ko!42p*28MtOmm$sv`?3v$5jX2dNYcd+6eh!`L?@<&uo7pEr49VMLNLV z&S(jO* z_KLD|mvYDrsghu44f0Z!7(Zm|3!$h<^6fwP7xFhqFq9lC!0e~*HHB6NWOV8I zgSiG*u;E{6laaAIuI>Zn_(;7XiV~x7o{$}}U=|tS*0>yxCQr3WLn?-du~d(wpM|CC zOB%h0&tQ}&c*_x07Bh1%{27PWBY9dht)@HMvvATct+YEpiLqg!*qPc)L1{ic0=YHp zC2c5hE$1)!!+TUc3L(V{bq<*7&QSzkmxRiYVkJ~-08u4fY@VrwW7mrf>|KaLloSeE zH@F1}&51%WMZN_+_I!5st{L=u(JV@(c{r_t{l>ZL*S2*uS!9(QUVy2v+LFfNbnMG5 zn(S0d3>`jbS~RP)#qPRfew#Q}&utR|zf6D{ZMc_BA^Cp5epIlz^Kxsy4YJoq7;y~0 z91Bq-HQ0Gpi8VjEvq$FNWLp??4l235U!hji!yK17EUea73;_*ouO{ zjfI80R!7W5{*ZxD8DwFOfd7x1to(RmSQy816RFe(`NQySf(jq0prT~EUiUbqM-&0G-~yMLR`*_IeeHObs; z;Xt>MQ!Jlx!^z#e%bY}$9b2TK5-r47H+_nFt_RxLDlwW@z<36PStjLIkGq!nC+H+Z zNn~v4t#PsT^?7=HlFs5%$FiJ4(4)vHWYolC`UNaO*K7||VkbfpMBKDF*OmBPo_{7^ zd00PaoILQ#nO_)@&KWiQhSy=-$yU+|n9q+-3jO{dlNEVAg}-nULkwHj4)hu&3iVjK z65Gnxmpq$Jr>6+@2rimAHwMt}g#jg7Ek;=K+eM3v{U{G=V`sOy80xI_b!bx*!T-L@ zt&lEV^$BW1VUrH>Lp#-wsm|Q%LC@9k)R`AWL380{l&vn6=jb(pR-;w8om-CK-`|HQ zQU-vL{;9%=SeI9vU+V>)r%{)&geh~tNuS7AJ=L(BpX6G3A#AuRfKD?HTH&**G(26Y zufj?aqGswI$3HKSP@|}N^X{;PN@*|a1)@vFoA#o^}Pwy#CoxJ1;rkEWE@;@1b z4XROxBCampsPyeW97GAG$3%J zqPGTq-Vn0ix42CV>#cqlBGKQ<`D8 zFm>D5xTxq$+IrHdD}k&{NvoTo!Vv*~+p=6_VL7PcRVXx08D{ZLSQWY$m{)#^M*9+n zxS)J(=b)c92V~7Y%ki?#$RF_*gdjUDoKw9Q0S!ldIA|Pn=$x9I#oNx0l4G*U*ox{oZevrc3U-xJeRRsJ3)Dk+FeUaj6y*61fql zUyjh2vEb6Q2T-qdqZin&N|tXlATQk#3waZmzA@xb*I{liJqe~OKN3ksF;w`F;KUaa zcL*=lp@7R7O5B}Y>q7~pRB#o9UXp;nJ1d(m3ln`xKa=c<$YPCMKB zlmclL9&pUB)Z)lMC2}$(OhF|kU1k&L{w2#22x4my zu~4Z5(41DoV3IyV4w1AioPyzfJqzPNBq$;S}T#-TQ6|>9e`+9)b>uJjQ8N0AU zp5Wr11y6pvMsY;M5X>zlJaCzbi#-S?;zm#hOP`~Tu62(dCA#R6tD3j#VI=uJNTSJ0 z1@y0%GGQ6V_E|em!!kZafGRP5FC||C%6f7_wxZGKhI4>`?Ir%Iyin)wb0#3cV0x%` z+Lx#K^p<|8iy9*A?O}6ITa2xoBQTMO^TC=cZM>gAy>97yNe8Jm)Jb@-xj_@ZzVNnM zm1r&rKw&^KGxYK&rb!Smg5d$s*;bwa)Z)WmRmq;D%flbY%~R`v!0&e7m5N6+ z$bFYKE%ZNa;hz=)#&U^ulMaEtN*+$uF5mwSr$+Nwr-T6WoV_@ib8#hzdW-P(- zDbNrVWR4S8F+p)~XKuR-Oev_|r?x&9Sf z8F1m&bmL_xV0I92fw$-H(;$a(bN_ASMlWh z(P9@va#gQ*;fKJ)jdruHCb^S>R9rz!8PKKtXD(G2o{~relpe;fRnc#S9B~KZwtduY z+o(sbqHHg`YJem;p|IZhN`kQ;8$mV1qLA`(ZVIbA@~S`V8m=dG5oH^Rb` z8am7UiWk%V1KBeF^x4l!Qb%u!plGz;Op^AvQ_$;R@U&z`I zw&ZqFZb5J?dPa3!6xh&F$U>9TumnvTQH-V|Kd{XJM^-Qev3< zYU9h8SD@qAvg%MjRs))xNdsDZFMyy}^Xfu_%gK8os9<0Q)Scs0$$jTRL8o1dF!L*Y z^!q9V{zA>LX4p48I9#}E20_XP(g|8#h5U+!s5>AxR#ipl9BQ1kl~?$w?9oQMpE+JN zFWL2Uxgzi|!lQ<|61fC$xxoQA7r=$<3!dgAuXR5x;&!-p?^tj_Mu`AKI#QM0uNi7^ znB3pKxp^5$M! zUIjb(HYg`%db^V@=erh1D8&i^4!o{u))s)=HiCQ$0=)G1K$!XFyD(Fp`T~5`S1eg% z;8Y(>2#C@KlBgF!#uP;qg<^tIfyNi|!U}kH^*BO6BmBp35oC|Gp%wQw-LrtN5DArF z^sYVSZ78rLfCvzj-}kwq$L`Q2T8W#qT7ewH-rb`grQ%T7p$p*d54U}H@!9k@9+-7b zwi4&T#=htIFxFFgBStD%6Woa4Ch1Ps@`S`s7kmwOjU1=Ts4Go@(=K7hdXdN6G>Ror;( zbUf$+iwbnK6BGx?#UdgxhF0~V%Rw&XJ*p6CQ&~|B2-AN}SyE7rE#O#=IenXiplY|y z?DqF%buE7YQ;Fe|-*DC$n$JYghprxWyRFnsZXXMsD1@c{G2#Kvb`3 z>gr7ZQUewAbb5?jh>tjS2n+7j#Z$Kl#1PY6xqaK}(knR*7>RO@V~=g50`G=%xwV9y zgs(Z&hpn*I%^#+|ZRUAT{kqxUFmnz*EpQ7PuY+5P{2|IZ{at;`dRtM2Pf 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 dce43472..9e490166 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 96616e8c..0438f9d8 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 f24d015e..3dc81c8a 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 0438f9d8..96616e8c 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 f24d015e..a50d2b5b 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 00000000..3b01492d --- /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 5410e0ff..635851d0 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 d0a2ea60..0bd6934a 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 05b34d78..77221bdb 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 5186aff4..de91b091 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 3025d722..f8ee512d 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 e98c6636..d483a185 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 64c37709..7dea516b 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 70a3eb6c..97a3ebc4 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 ae943e1e..bab81914 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 1743505c..05d02dd5 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 37c08a97..4bfa8634 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 f457833f..47eeb336 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 939a4831..4634cea8 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 8413673a..2d0f29a5 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 39ee3bc7..4e0e79aa 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 7cb678ec..09284738 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 25562f55..16c99d20 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 68a80e54..f4be0396 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 a4ea5830..15f7b977 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 7069c0da..acf1c9b9 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 43eb552d..e1bc4318 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 49c7b991..48829053 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 41239961..e6ba5b17 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 e69a140c..a1ad932b 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 0887a591..5ece4600 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 304353d4..e9b83e29 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 1792eccc..a8ef18d8 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 6f6f7a18..47a79534 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 2fb74fa5..1eb33b97 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 5d4a6e1a..1ade9757 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 31a3ade8..e5704093 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 e30c09e4..06389207 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 b8861807..4190ba39 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 1c75aaae..629c5ac7 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 a9e2c171..b683fb78 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 7c49a29a..e4957e3d 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 92c99d90..3c1d7776 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 8bb1d45e..76f61142 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 447c2e31..1725a8f4 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 25c15f52..97db904d 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 00000000..feaf416c --- /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 03491be6..830ef0d3 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 00000000..1e9a3071 --- /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 00000000..8a87116f --- /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 00000000..73d79cf0 --- /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 00000000..f2b3ae27 --- /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 00000000..569c6872 --- /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 00000000..2563015f --- /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 00000000..14546b97 --- /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 00000000..ba4ab532 --- /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 6bb49000..77d687d9 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 105b8440..9ca5a16f 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 edf0ef49..2b19bbaf 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 ed270638..7e3d0b0a 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 9ca5a16f..02f34132 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 02f34132..8aa5f72d 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 0f40cb7d..18ff433a 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 f0f66e28..2fcdef29 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 6bb49000..9f8265fe 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 b4e79b36..0e307abe 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 8aa5f72d..7e4ba2b3 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 2e1108b9..115cb978 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 7e4ba2b3..4dcbe5c9 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 59f503a1..67267bd9 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 2fcdef29..b308de14 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 9f8265fe..1a1a558f 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 1a1a558f..66a13f60 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 66a13f60..c68b394e 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 26e25a20..2b4bc7f3 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 27bb0377..fef0dd3d 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 18d42d14..5817e54f 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 67267bd9..917a815b 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 8cf90eba..a1d9ff0d 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 7a6bcbc1..a153dbc6 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 feaf416c..5403853d 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 830ef0d3..7088107b 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 1e9a3071..7488acc9 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 8a87116f..b5a88106 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 73d79cf0..cd01f93f 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 f2b3ae27..8d441489 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 569c6872..d5481d7a 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 2563015f..dd718b96 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 14546b97..33a57e9d 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 ba4ab532..9e9e3de8 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 211d36c3..dac62032 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 9b00b481..610a7d1a 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 666c9a6e..05c3059d 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 00000000..ce63dcab --- /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 77d687d9..ff586a1f 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 00000000..39367c3c --- /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 00000000..42a11fe5 --- /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 dac62032..211d36c3 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 610a7d1a..7bd2a412 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 05c3059d..47de1be2 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 ce63dcab..b2871e0c 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 ff586a1f..5029560b 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 b2871e0c..904195f7 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 5029560b..afc5b2d9 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 a1d9ff0d..865a8bea 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 0e307abe..3f994085 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 904195f7..7c9fe67e 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 189f58f4..933850c9 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 865a8bea..39b1cdd0 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 a60e379f..d0f397d3 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 3f994085..53ddb724 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 ba7e3d76..bdef5f63 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 74248b5f..5427305b 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 ba7e3d76..8a33f214 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 933850c9..0fa12cd6 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 009995f4..1fb69439 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 9f375860..c01fe891 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 28d3ae62..458ca29f 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 d483a185..3220c824 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 7dea516b..d0fdcb3f 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 97a3ebc4..aca4778e 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 bab81914..7b959a77 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 05d02dd5..1672af2b 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 47eeb336..9c9097f6 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 4634cea8..dd132287 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 2d0f29a5..db565fe2 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 4e0e79aa..fd46b4cf 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 09284738..90c8e105 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 16c99d20..78648a03 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 f4be0396..d06cc649 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 15f7b977..57215e2e 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 acf1c9b9..6e72d4b0 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 e1bc4318..b7b59ed9 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 48829053..9fdc2926 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 3b01492d..2502cb34 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 e6ba5b17..24563d21 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 a1ad932b..078bf376 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 5ece4600..e08700d4 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 e9b83e29..5be2a914 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 a8ef18d8..4af36295 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 47a79534..bf4b85b1 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 1eb33b97..f464cb8f 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 1ade9757..a6b83d9f 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 e5704093..09c34b4f 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 06389207..2154b272 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 4190ba39..f46a301f 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 629c5ac7..93e984be 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 b683fb78..214ee83d 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 e4957e3d..db26e538 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 3c1d7776..c3894726 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 76f61142..45c2cc51 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 0fa12cd6..2b7f8c00 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 c01fe891..0880fdb9 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 0880fdb9..c2f8f9a3 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 c2f8f9a3..85aae4c8 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 14a2c38b..43dc3a65 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 77221bdb..97012e51 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 de91b091..427d0a72 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 bd1f4f1a..c8f875da 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 1725a8f4..a86f07d0 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 97db904d..947f8fb6 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 947f8fb6..2344f84a 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 97012e51..51e7f9a2 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 427d0a72..c673531e 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 0aa7de07..f3bc4585 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 4dcbe5c9..3f7ed677 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 2344f84a..b225151f 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 378ddbd1..25c485a2 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 dd132287..63c1d26f 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 57215e2e..ab0c8064 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 00000000..6aed6053 --- /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 2b7f8c00..3bec6862 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 d9afbea5..b5cadbcd 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 4af36295..c389d682 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 5d4cf39c..78c60df3 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 1ab21dbf..ffcadd18 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 47de1be2..3e85cb29 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 7c9fe67e..96cc9fa9 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 2b7f8c00..c97ef9d3 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 458ca29f..1afb5b14 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 e4c9fa03..761c95de 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 7e3d0b0a..2a3fd05b 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 c673531e..b51c481a 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 ee44cf4f..02d367b5 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 53ccd700..1a974c51 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 bc9c5ff6..52b1717c 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 1fb69439..e25c0d54 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 a86f07d0..4794efb6 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 b225151f..5a83ee57 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 d7d944cb..e82e9d26 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 5427305b..1738a02e 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 1a974c51..2e2bce3e 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 2e2bce3e..9cdbf974 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 04b2ccc9..9b25f497 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 00000000..65291b96 --- /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 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 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 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 SCmh2&11QO<%T`L82mcS4z>B&7 literal 8908 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 ATmS$7 literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/flutter/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..ab983282 --- /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 85aae4c8..e1dd1a1f 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 1672af2b..38f4fdda 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 c2d92097..a955c2a2 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 a955c2a2..ec23aa7a 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 50cd6d0c..29b0d0e0 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 0381ff96..a96c782d 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 25c485a2..34398dd0 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 a96c782d..86c11ccf 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 cc348f38..426127fd 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 a5bd1de8..ef0bb12a 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 b7bb2f42..df394a84 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 0fa45b93..931e7284 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 5403853d..3049f3b8 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 6aed6053..4185945e 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 e46dab11..6fd89c9a 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 39367c3c..7c07ee25 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 42a11fe5..530f12a9 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 7088107b..c4ab3c92 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 7488acc9..fb18eabd 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 b5a88106..9d107d69 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 cd01f93f..93f27bf7 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 8d441489..f244631f 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 37bb196e..5a344dd0 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 f08bee6a..17b474e6 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 69f0c96c..163e045e 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 09a055be..cfbb046c 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 d5481d7a..d72033f6 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 1738a02e..2c3697be 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 13eb73f2..4d43f8bc 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 8092b3af..ccf9c7aa 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 dd718b96..a8715011 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 33a57e9d..09aa55e2 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 e1b96212..bbd948c7 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 29e1d3c4..fcd99f2f 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 8ecbdb47..af54808a 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 9e9e3de8..7e93a7a3 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 5654befc..bf90ec95 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 fd8334f9..f1771c3f 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 326f7829..a0f7e3de 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 e537d41f..8ee4a51f 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 b308de14..eb26f2ed 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 9588d10b..0ebe49fd 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 cc4173a9..fc4db9a6 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 2115d728..f0f7d49a 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 59f0896c..e7a7eacd 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 1c49adfb..99cb6f40 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 8daba257..05ecd292 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 e2eb4317..82cb88fa 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 504f0a4b..77070d1a 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 5be55ea2..cf442c2b 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 11a6d5fc..d5f992f0 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 ca39c0a6..6b8c6e0e 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 0dcfcfda..6486af55 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 5df5c46a..500f5761 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 cb3310be..406c2735 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 51e7f9a2..8683dad1 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 e2f3f80e..7d722e6c 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 8fa95ac9..2ff2d372 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 c783fef5..fd17fd46 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 eb26f2ed..48981e16 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 e7a7eacd..0457bb19 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 00000000..26320e32 --- /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 7c107d11..191ea2e6 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 00000000..299a21f9 --- /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 05ecd292..89a3a156 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 7d722e6c..2619a1c0 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 26320e32..347bec47 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 299a21f9..0008c626 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 2ff2d372..32c32efb 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 fd17fd46..7514ead3 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 b449bbbd..12523605 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 26038218..f44bb4ee 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 86c11ccf..92eb7a9f 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 426127fd..432d0513 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 92eb7a9f..6b86e88d 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 cd618dfc..0a1b1dcc 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 347bec47..0be78842 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 89a3a156..0a4299ae 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 0a4299ae..b65980c1 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 0008c626..dd83a873 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 b65980c1..aa929ca9 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 b65980c1..aa929ca9 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 e96a2b40..cdce6f12 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 3220c824..0d1eeff1 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 d0fdcb3f..63b59e8f 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 aca4778e..f4d63cba 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 7b959a77..b3bf02dd 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 38f4fdda..ddc34760 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 9c9097f6..99752b3b 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 63c1d26f..ac367898 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 db565fe2..1d2fbe52 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 fd46b4cf..ef76a8fc 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 90c8e105..9a813cd0 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 78648a03..31a6d8d1 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 d06cc649..8176c9bc 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 ab0c8064..2431da44 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 6e72d4b0..a5179523 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 b7b59ed9..b6e992fa 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 9fdc2926..aafec8b0 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 2502cb34..9a239238 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 24563d21..be61e94e 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 078bf376..b4befcdc 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 e08700d4..3fe0ca86 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 5be2a914..b06d1fa0 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 c389d682..9746e8a4 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 bf4b85b1..27bf78dd 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 f464cb8f..4ccc9e35 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 a6b83d9f..347d1279 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 09c34b4f..19232b1e 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 2154b272..da7f4df4 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 f46a301f..e988b648 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 93e984be..57080641 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 214ee83d..393357ec 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 db26e538..17cafb8f 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 c3894726..7eeca7de 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 45c2cc51..3affb52d 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 3c9a438a..f1b94ecd 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 34398dd0..187ffc9f 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 187ffc9f..971c713c 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 6b86e88d..6d00302f 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 01ff2348..0a285429 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 63c1d26f..3a467cb1 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 8683dad1..6e4033d7 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 2431da44..2d66706d 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 9cdbf974..d2eb21ee 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 2d66706d..68ec1080 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 6e4033d7..f36bdae7 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 0d1eeff1..45c55284 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 63b59e8f..9d0d176d 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 f4d63cba..e2761e45 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 b3bf02dd..2020a2b6 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 ddc34760..7cf563fc 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 99752b3b..c2253244 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 599da6fb..3ce2860f 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 1d2fbe52..00f6b70a 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 ef76a8fc..1f6e9f55 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 9a813cd0..b7ebf457 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 31a6d8d1..21ab2821 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 8176c9bc..f48de17f 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 68ec1080..4c63106d 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 a5179523..b291a6e7 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 b6e992fa..d63e8318 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 aafec8b0..b8b9eb1d 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 9a239238..1a806c80 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 be61e94e..2b29c7cb 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 b4befcdc..e91cd390 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 3fe0ca86..b0fe9175 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 b06d1fa0..d0232ba3 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 9746e8a4..6df73f1e 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 27bf78dd..458002f4 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 4ccc9e35..2abd1870 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 347d1279..6b739e8a 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 19232b1e..90a435fd 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 da7f4df4..a98ea634 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 e988b648..61c2b5d2 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 57080641..236ee5e8 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 393357ec..f2a34e21 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 17cafb8f..84e74716 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 7eeca7de..0c4caf4d 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 3affb52d..19e1184d 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 9746e8a4..34a43346 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 86606372..df0ca832 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 6d00302f..32a440b2 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 00000000..a41d4dc3 --- /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 432d0513..a5c5c8c1 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 00000000..a206c360 --- /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 0a285429..b7a475d6 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 a41d4dc3..7a85b6da 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 a5c5c8c1..cd82c75e 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 7a85b6da..df87aace 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 a206c360..c972f47b 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 ec23aa7a..fea1a367 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 df0ca832..5e4c5e70 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 c9cf1f25..ca1c9a9f 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 ca1c9a9f..f31a16de 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 b7a475d6..c6b639f8 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 48981e16..115845b5 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 0ebe49fd..f685e3f2 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 0457bb19..a125078d 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 99cb6f40..bfb77390 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 9cd6077a..27b157b7 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 699b0bcd..b1b13034 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 0c8c5145..910c2698 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 bd2a0129..88a5aaaa 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 5833e760..dd39cbdf 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 2b4bc7f3..a4cb5002 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 b5cadbcd..ff99c9dc 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 64c78f24..ef3a0dd0 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 e82e9d26..adbf50ab 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 b1491d52..9a5b06b1 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 f3bc4585..68ddce9b 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 adbf50ab..45857aa4 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 9d0d176d..78a1f9e7 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 78a1f9e7..4824ac5e 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 9ba7a631..357abab2 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 df29252c..a4584f4a 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 357abab2..ee3aaaf2 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 a4584f4a..e009ea89 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 5e4c5e70..a1790107 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 a1790107..c081ca9c 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 00000000..067e0ecf --- /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 2e1702a6..bba11431 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 78c60df3..2386f17d 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 c081ca9c..419a91f9 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 419a91f9..8af79915 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 00f6b70a..70051f3e 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 e1dd1a1f..ff8dfbb0 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 2b4bc7f3..22ba221a 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 4edffb3b..262121f3 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 7540f766..bbe2b28b 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 96cc9fa9..df2c48ab 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 91a061fb..64c44a55 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 df29252c..7789f92f 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 262121f3..d42a2829 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 00000000..d0f032bc --- /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 00000000..19563b84 --- /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 00000000..21c7fb9d --- /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 00000000..3959f787 --- /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 00000000..22b72920 --- /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 00000000..45a018f5 --- /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 00000000..f77fcfd4 --- /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 00000000..295136d7 --- /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 00000000..f9037e0e --- /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 d42a2829..0d55552a 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 0381ff96..32a440b2 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 00000000..df87aace --- /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 24ba9a91..cd82c75e 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 00000000..c972f47b --- /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 ec23aa7a..fea1a367 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 2e1702a6..bba11431 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 78c60df3..2386f17d 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 48981e16..115845b5 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 0ebe49fd..f685e3f2 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 df0ca832..8af79915 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 00000000..067e0ecf --- /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 01ff2348..c6b639f8 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 bd2a0129..88a5aaaa 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 5833e760..dd39cbdf 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 22ba221a..2b73182f 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 b5cadbcd..ff99c9dc 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 64c78f24..ef3a0dd0 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 e82e9d26..45857aa4 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 9ba7a631..ee3aaaf2 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 b1491d52..9a5b06b1 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 64c44a55..a07df9c2 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 7789f92f..667b3645 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 0457bb19..a125078d 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 99cb6f40..bfb77390 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 9cd6077a..27b157b7 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 c9cf1f25..f31a16de 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 f3bc4585..68ddce9b 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 699b0bcd..b1b13034 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 9d0d176d..4824ac5e 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 00f6b70a..70051f3e 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 0c8c5145..910c2698 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 cd917b5e..572b3e20 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 f36bdae7..aa352318 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 68ddce9b..7eeb96b5 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 115845b5..eb5461a6 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 f685e3f2..0a7af0cb 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 bad6e000..cab7a900 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 cab7a900..2888ffe7 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 ff8dfbb0..6d3e4c3b 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 f9db985d..df987417 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 1afb5b14..0b6f1463 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 ba62b775..13f5b458 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 2888ffe7..f5d764e6 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 4794efb6..7b31c84e 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 5a83ee57..5fbf2f4e 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 df987417..4a2f5c0e 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 0b6f1463..a38db2a9 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 f5d764e6..a5689bce 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 7eeb96b5..c55866db 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 4a2f5c0e..c78ffb43 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 a38db2a9..5ef72a0a 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 acfd4c67..3adc24a1 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 2b0223a0..a2177805 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 27b157b7..d2b9f414 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 406baecb..77c21ffc 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 45aafe7c..c7da5773 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 5164886a..7a65b193 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 aa352318..9f4cef83 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 a5689bce..f78e1bd9 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 c55866db..d8861eb0 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 0a7af0cb..050a0cd4 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 77c21ffc..7bda0b69 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 9f4cef83..0c2cf09c 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 f78e1bd9..c232d891 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 d8861eb0..14906d56 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 a07df9c2..5ffe805b 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 9e490166..727b53fe 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 a2177805..f3ad51a2 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 c232d891..42da3f03 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 eb5461a6..14b09a9d 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 050a0cd4..b51930f7 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 3187c634..16dc0d35 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 21e87032..e9043da7 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 42da3f03..51c96ddc 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 b43b9095..2f1f36f7 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 0c2cf09c..ebfda728 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 14b09a9d..8483cbac 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 b51930f7..b424b01d 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 51c96ddc..f2f950ad 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 f2f950ad..c501bd4a 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 c501bd4a..d366a0ed 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 ffcadd18..b0819397 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 c78ffb43..e5233451 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 2386f17d..74e4efa9 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 b424b01d..c2036698 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 bbe2b28b..148d928d 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 12523605..5e162003 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 910c2698..3e19cca2 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 5e162003..617ec2c9 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 f44bb4ee..dd111f86 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 f1b94ecd..7b24ec2e 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 7b24ec2e..6ea6a97a 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 6ea6a97a..4a376c58 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 4a376c58..9d9d3d01 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 9d9d3d01..fd049930 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 fd049930..325dfd2e 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 325dfd2e..657ba3cc 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 657ba3cc..db2a90d9 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 db2a90d9..8d4d5877 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 0d55552a..39d66f56 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 02d367b5..5f24fd5c 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 6d3e4c3b..e1b9ac90 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 62f81b79..c1991633 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 f1b94ecd..0a175139 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 4080f9c1..da7e37e6 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 646ee2a8..4aad66ee 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 ff99c9dc..dfa5762b 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 971c713c..06a79093 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 35d5a61e..053a2d8a 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 2ac6bf23..ae070b47 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 ee5dd9b5..f2d75d00 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 e5233451..ab0daece 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 ef3a0dd0..0deb646c 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 252e1cd1..45591b79 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 1f6e9f55..50cb2993 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 5ef72a0a..f4efe2f0 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 2a3fd05b..be3a1e51 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 d366a0ed..ea73eb92 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 14906d56..8a8bf4de 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 ed5fcfaa..ad058d4c 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 bd6a1fc4..6b3f8013 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 d2eb21ee..85fcb676 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 52b1717c..a9a9fd9a 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 5fbf2f4e..fd5a7d9c 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 8483cbac..a2cdf91a 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 c2036698..f93f776a 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 61112bff..6e3fc94f 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 0c5ba503..a33903ca 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 500f5761..b34fed41 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 406c2735..28609376 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 32c32efb..08e343d4 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 789404cb..44335146 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 3e19cca2..02527484 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 6d3e4c3b..ddd9ea1a 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 971c713c..ffe707cf 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 45857aa4..993d0268 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 45c55284..71aa3933 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 4824ac5e..818e6320 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 e2761e45..be0ffa7f 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 2020a2b6..150a5771 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 7cf563fc..c9c25df2 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 c2253244..bb2615ef 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 3ce2860f..d7e43b6b 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 70051f3e..d8fcff43 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 1f6e9f55..cb4d8d69 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 b7ebf457..c18e6c07 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 21ab2821..557e3faf 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 f48de17f..1a34e6fe 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 4c63106d..7256b13d 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 b291a6e7..d6354c1c 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 d63e8318..dc57c8bf 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 b8b9eb1d..6698b2c5 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 1a806c80..545e1ec2 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 2b29c7cb..eea46acc 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 e91cd390..ee156112 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 b0fe9175..7b16bdf3 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 d0232ba3..315eadd2 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 fe5d708a..6d212490 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 458002f4..462a78ab 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 2abd1870..0eb1949f 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 6b739e8a..2fc5dfe0 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 90a435fd..17882094 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 a98ea634..250cf340 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 61c2b5d2..dcdcc128 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 236ee5e8..a1eb34c5 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 f2a34e21..09c40a83 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 84e74716..ca1193ea 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 0c4caf4d..b48385e6 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 19e1184d..61d7c0b8 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 993d0268..b32520fa 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 b32520fa..4f9a227b 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 8d4d5877..470b631c 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 8a8bf4de..23a65c2d 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 1b6838e4..a197cb25 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 dd111f86..3b2ba089 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 470b631c..5b6120a0 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 23a65c2d..0866ff73 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 4f9a227b..bdf9c1f1 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 7e9a9879..93199938 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 f4efe2f0..eae41679 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 b51c481a..1c778819 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 ea73eb92..2f660775 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 85fcb676..b2e19868 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 7b31c84e..c6e0229b 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 fd5a7d9c..f726ed52 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 2b73182f..53778491 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 0d55552a..68023f92 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 00000000..3821b7e0 --- /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 9598c340..36128bf2 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 68023f92..569e1cb9 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 bdf9c1f1..8710d27c 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 8710d27c..37bbbd66 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 b2e19868..898939b6 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 08e343d4..47184e79 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 9a5b06b1..fca73eac 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 7256b13d..04b2488a 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 9b25f497..ede6353e 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 4f9a227b..329df4e1 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 01808a15..257ba417 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 37367c19..f764aa3e 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 818e6320..fd2e04c5 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 39d66f56..a709ccba 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 a709ccba..96ab4044 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 96ab4044..5df9c9a8 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 df2c48ab..17b160fe 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 00000000..85e6ff19 --- /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 00000000..f0204277 --- /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 00000000..02c6e589 --- /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 00000000..ea8c4bf7 --- /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 00000000..d4254717 --- /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 00000000..62c96412 --- /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 00000000..3b29a10e --- /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 00000000..f79c691f --- /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 44335146..8be0c6db 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 02527484..b663b0f4 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 a9a9fd9a..affb5eb1 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 a197cb25..e0449fd9 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 eae41679..e48d74da 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 37bbbd66..8500bec0 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 8500bec0..c27546d9 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 d7e43b6b..5ae36858 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 ca1193ea..e20c7f25 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 ff373cc9..e8967171 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 4aad66ee..edbd5b7c 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 0866ff73..e49ba65f 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 e0449fd9..22c44ec5 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 ec2e0a74..0e224707 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 c6e0229b..ed16f1e0 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 3b2ba089..62eba25c 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 eea46acc..fe6454fa 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 00000000..24149bf5 --- /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 5df9c9a8..c3322fef 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 45c55284..4a6536af 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 4824ac5e..593e023d 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 e2761e45..d09bba09 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 2020a2b6..428150ef 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 7cf563fc..8bfa973c 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 4bfa8634..3e87cd66 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 c2253244..1ea05d5a 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 3ce2860f..494dd4cb 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 70051f3e..72dd93a1 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 1f6e9f55..a600c92e 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 b7ebf457..26e5a649 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 21ab2821..125d982d 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 f48de17f..17a9589a 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 4c63106d..0749603b 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 b291a6e7..7c810cba 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 d63e8318..87425b40 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 b8b9eb1d..49017f4f 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 1a806c80..615c3601 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 2b29c7cb..ae1743ee 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 e91cd390..4e619a6e 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 b0fe9175..8f4b2b13 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 d0232ba3..ebefd1a8 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 fe5d708a..d652904c 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 458002f4..47843fd5 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 2abd1870..95d65a45 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 6b739e8a..3ec67d77 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 90a435fd..31f40874 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 a98ea634..3afcf1a9 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 61c2b5d2..53df7b4f 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 236ee5e8..a5db9708 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 f2a34e21..96e35e2b 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 84e74716..f49e81a7 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 0c4caf4d..1729d42a 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 19e1184d..5521022b 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 5269c4ee..13027a68 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 f93f776a..b53615c4 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 02c6e589..f0016b5b 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 d4254717..7b7cf26b 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 62c96412..6a67813a 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 e8967171..53a40123 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 fca73eac..37c4a39d 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 e48d74da..74cc7f14 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 37c4a39d..b9145313 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 ee28fe0e..3d95832e 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 8ee4a51f..dd2aa860 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 00000000..6258a9a7 --- /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 00000000..2a0d73f1 --- /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 8be0c6db..ac9a69d0 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 b663b0f4..5c4c68e2 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 62eba25c..471150f6 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 ac9a69d0..3c90981c 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 53778491..789517bf 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 f2d75d00..394d89e3 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 0deb646c..e7aed035 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 bb6bc431..64af4140 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 ee3aaaf2..958c4c03 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 761c95de..7de258a3 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 572b3e20..b10dd1ec 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 74cc7f14..21949705 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 53a40123..a73ab8bd 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 badb68a8..31f9e154 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 789517bf..b43f3e7d 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 053a2d8a..4a1a4024 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 148d928d..39958e88 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 394d89e3..32f02c9b 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 e7aed035..d810650f 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 7de258a3..aa4fab86 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 b10dd1ec..8d390d37 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 b43f3e7d..3c414cd8 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 eb7c214a..f38c1479 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 e7bb2881..d7202e30 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 c61287d4..baf7193b 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 727b53fe..b30f76f9 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 16951542..3759f605 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 6b3f8013..0bba649f 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 7514ead3..c49f974a 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 07b8af99..3bfb5357 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 ede6353e..6f646b91 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 32870156..a49dcc32 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 fd340f7e..73dbd2da 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 cf8e12e9..e2831196 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 00000000..3beb7ec6 --- /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 4bf244a0..a812686e 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 d74aa35c..146267c9 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 b0819397..ffcadd18 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 21949705..fd97b1e5 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 73dbd2da..7050e7a4 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 a812686e..0bc6c1c2 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 53a40123..41ac595f 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 3c414cd8..a0766a87 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 abccdf68..648448f4 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 c5f3b693..6bdb7e81 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 b2043f3c..433990a2 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 74e4efa9..cae5b82c 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 a73ab8bd..3680b0a1 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 31f9e154..f276961b 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 5817e54f..7e702f6f 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 af0da047..a95d3977 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 6f646b91..b3c65591 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 0bc6c1c2..2d53ea01 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 a0766a87..1dc1c0b5 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 6bdb7e81..0e160396 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 3e87cd66..25053001 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 fd97b1e5..cea6785f 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 d768005f..f93f776a 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 c27546d9..4b021570 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 b9145313..8c6a8937 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 cea6785f..6def5746 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 e041b591..52f64c0e 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 a2cdf91a..8f8895bd 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 32ed149e..c5bc3394 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 53a40123..023fe751 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 0c31e153..f76567ee 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 41ac595f..89b2c1b2 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 1dc1c0b5..3edafbf6 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 0e160396..397c117f 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 433990a2..7ee23ec4 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 b30f76f9..8d64553c 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 126e169d..e4b42cf0 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 2d53ea01..0c34c222 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 89b2c1b2..155fcc74 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 3edafbf6..b075ee76 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 397c117f..d4887bb5 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 8d64553c..45fe1b13 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 e4b42cf0..be8c857c 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 0c34c222..6970fd13 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 3680b0a1..29d4a195 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 f276961b..9210a30c 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 a69fc3bb..1d3a18c7 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 a49dcc32..8f6767e5 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 be8c857c..79fb6079 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 e2831196..e323d295 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 6970fd13..bd91d582 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 d4887bb5..e54f66ff 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 e323d295..6b6169c6 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 8c6a8937..df9ad258 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 6def5746..802a18a5 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 1d3a18c7..13321db5 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 13321db5..cc5568bc 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 93199938..fde9ac4a 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 0019335e..c73e666c 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 8f6767e5..71bbba75 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 79fb6079..52a5ff75 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 6b6169c6..fa7440c8 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 bd91d582..f8ef07fd 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 b075ee76..95e4d17e 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 baf7193b..6d3f863e 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 13f5b458..28dc8085 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 e49ba65f..e5b24fa5 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 e54f66ff..e07f8f59 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 aa33ae6e..53ec69b5 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 f975e343..4c037234 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 cfe69924..25a494ee 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 19310357..8fd6f9be 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 3d95832e..754d7b9e 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 9b7912cf..dfee4fb8 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 af0da047..8477ba99 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 0c31e153..2cefaa98 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 0e45827f..28f1dd9d 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 fca98f22..55a3c9bb 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 437cf445..f47d522d 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 84892a7f..7d02e154 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 101685c4..8aedc04f 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 c19b607c..d097a8b6 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 97574e67..8ca881f1 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 54a51b43..a9acdce6 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 f38c1479..cf3eb430 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 13027a68..a0808f5b 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 923bbab0..b62bd5a3 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 aa491f95..546ef2a3 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 e992b19d..af9389a2 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 3bfb5357..b9af4ce9 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 6468b7ee..8a6b765b 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 d128e732..5721d01f 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 29c5cbbf..1c488d47 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 63173dc1..249c0b59 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 1a00ece4..90ec8c1c 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 2c83f947..6563d605 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 6fcf02ed..31662239 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 d35d288d..7359bf06 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 20a2998e..70533c48 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 4c4b5d4b..6b54c83c 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 32cd084c..a379b318 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 c27546d9..90b48160 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 fde9ac4a..6b87d62b 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 481d6555..aa5f99f8 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 648448f4..bab2911f 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 f236a782..47d58ce5 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 0bba649f..561bb457 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 00000000..95169df8 --- /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 7bc82ed9..6fb2c895 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 6fb2c895..ed7270a8 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 cf3eb430..c0e627c6 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 c0e627c6..d1c15454 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 a0808f5b..49471552 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 d7202e30..00ca2bb2 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 958c4c03..edc779fb 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 6d3f863e..bb1b4f55 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 e0306a3a..e049c0fe 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 52f64c0e..7d907d72 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 5ffe805b..76fe929e 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 8d390d37..71a840c9 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 e049c0fe..c438a3c7 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 7d907d72..0aafd48b 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 29d4a195..47850fdb 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 cc5568bc..5a7f2bfa 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 9210a30c..9c72caa5 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 6b87d62b..3832ca7b 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 7e702f6f..a899f410 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 47850fdb..eeae0972 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 90b48160..49c56466 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 abccdf68..1a77b898 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 a899f410..56c9339f 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 74e4efa9..cae5b82c 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 b0819397..ffcadd18 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 a2cdf91a..8f8895bd 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 f93f776a..b53615c4 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 727b53fe..45fe1b13 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 00000000..6258a9a7 --- /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 00000000..2a0d73f1 --- /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 8ee4a51f..dd2aa860 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 ede6353e..b3c65591 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 32870156..71bbba75 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 fd340f7e..52a5ff75 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 cf8e12e9..fa7440c8 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 00000000..3beb7ec6 --- /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 4bf244a0..f8ef07fd 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 d74aa35c..146267c9 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 eeae0972..ceff7480 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 53778491..95e4d17e 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 e041b591..0aafd48b 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 053a2d8a..4a1a4024 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 148d928d..39958e88 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 e7bb2881..00ca2bb2 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 f2d75d00..32f02c9b 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 0deb646c..d810650f 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 bb6bc431..64af4140 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 49c56466..081cd164 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 32ed149e..c5bc3394 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 ee3aaaf2..edc779fb 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 c61287d4..bb1b4f55 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 1a77b898..218559a6 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 c5f3b693..e07f8f59 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 fca73eac..df9ad258 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 e48d74da..802a18a5 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 13f5b458..28dc8085 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 b2043f3c..7ee23ec4 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 761c95de..aa4fab86 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 0019335e..c73e666c 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 5ffe805b..2202b2cc 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 572b3e20..71a840c9 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 00000000..95169df8 --- /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 3bfc885c..ed7270a8 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 481d6555..aa5f99f8 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 ebfda728..40a9f05b 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 e49ba65f..e5b24fa5 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 aa33ae6e..53ec69b5 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 f975e343..4c037234 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 cfe69924..25a494ee 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 19310357..8fd6f9be 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 ee28fe0e..754d7b9e 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 3e87cd66..25053001 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 9b7912cf..dfee4fb8 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 af0da047..dc28cdae 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 0c31e153..824bd039 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 0e45827f..28f1dd9d 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 fca98f22..55a3c9bb 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 437cf445..f47d522d 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 84892a7f..7d02e154 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 101685c4..8aedc04f 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 c19b607c..d097a8b6 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 97574e67..8ca881f1 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 54a51b43..a9acdce6 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 eb7c214a..d1c15454 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 13027a68..49471552 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 923bbab0..b62bd5a3 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 aa491f95..546ef2a3 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 e992b19d..af9389a2 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 07b8af99..b9af4ce9 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 6468b7ee..8a6b765b 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 d128e732..5721d01f 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 29c5cbbf..1c488d47 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 63173dc1..249c0b59 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 1a00ece4..90ec8c1c 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 2c83f947..6563d605 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 6fcf02ed..31662239 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 d35d288d..7359bf06 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 20a2998e..70533c48 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 4c4b5d4b..6b54c83c 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 32cd084c..a379b318 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 16951542..3759f605 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 8be0c6db..3c90981c 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 b663b0f4..5c4c68e2 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 6b3f8013..561bb457 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 7514ead3..c49f974a 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 e25c0d54..257ba417 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 62eba25c..471150f6 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 f726ed52..11bcff92 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 02c6e589..f0016b5b 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 d4254717..7b7cf26b 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 62c96412..6a67813a 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 6c7055b4..77c28a94 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 73bb993a..6124cbb7 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 cd0e5871..01855536 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 ad058d4c..f2b609d3 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 617ec2c9..38ed9b0c 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 22c44ec5..f7419cd3 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 824bd039..158e16f7 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 2202b2cc..76fe929e 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 754d7b9e..d6ebe7f1 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 8aedc04f..05fffedb 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 0001d076..965218c9 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 5c37900f..9ba7a631 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 475b4cb8..38e4d917 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 0001d076..965218c9 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 4b6cf2a6..366fb2ed 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 117486ee..40ab2118 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 2ecfca83..49e3f235 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 79a4664d..b66261eb 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 a7e32d0b..a79ef2de 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 7b94c8a2..748d375b 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 6500a8e4..8bc37584 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 7973a0ba..aede5fe7 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 8a1fc990..cd0e5871 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 d357c9ce..6576c340 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 6576c340..26038218 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 748d375b..5dcd6389 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 8bc37584..16951542 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 17f275c2..bd6a1fc4 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 9ce53c96..53ccd700 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 57fdf2c2..bc9c5ff6 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 de33b016..f5c575d4 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 a731f0b0..4ad4a992 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 ca99a5bd..feab5bdc 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 24ba9a91..cc348f38 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 feab5bdc..add1289e 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 4b6cf2a6..5d524523 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 a7e32d0b..3611b5db 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 5c1e1218..5cd2af2b 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 4ad4a992..d86960a0 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 b66a08e7..252e1cd1 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 add1289e..eb837ba7 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 aab12ab5..b2043f3c 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 c37d0186..b1491d52 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 d86960a0..a295ad4f 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 1e4d80c9..3bfc885c 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 b66261eb..ee44cf4f 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 ce97745f..1b6838e4 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,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAEiuAABIrgHwmhA7AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAEx9JREFUeJztnXmYHMV5h9+vZnZ0rHYRum8J4/AErQlgAQbMsRIWBEFCjK2AgwTisGILMBFCIMug1QLiPgIYE/QY2QQwiMVYjoSlODxEAgLEHMY8YuUEbEsOp3Z1X7vanf7yR8/MztEz0zPTPTO7M78/tnurvqn6uuqdr6q7a7pFVelrkpaPhhAMTEaYjJHDUWsEARkODANGAfWgINEPxLb7QNtBPkdoR7Ud0T8iphUTbtXp4z8pyQH5KOntAEhL2yCCnALW6aAnIDQAI+3MqFHkGJM73BkCO93JXnQnsAl4C8MGuoIv69mj2rw9ouKq1wEgzRiO2noSlp6DoRHleISgnQkJnRpLw0sI4v9X4H2E9Yj172zf+2udOflgYUdYXPUaAOTpzxoImJkIsxG+YCfG+Z7cecWDIN5+J8hqjNXCIW3rdMqULvdHWBqVNQDS8tlwNPCPKJcjOslOjGZGt2UHQTStHZGnMPxQG8d9mOk4S6myBEBWbj0aZR7ILISBPRlZOiMlr+QQgGAhvITqg0ybsEZjhZWHygoA+VnbaSBLEaY6dgb0Vgii+h2GO2gcv7JcQCgLAOSp7ZNBlyI6sycR+igEILoRdJFOnfgCJVZJAZCf7pxETfhmlIsQjHNH9VkIAF0H1iKdetjvKJFKAoC0EODA9msQvQUYmL2j8uwMJ/uygwAL0dvZMHGJNmFRZBUdAHlix5dQfQw4IbeO6tMQgOgybZx4I0VW0QCQ5dQQ2v4DhO8Dofw6qk9DEIZwg0497H8ookwxKpEV7WOo2fES0IQSAnrmwBrXEhq/lcR5cnJasm1KWq5lx9knl5NvvW7877EPIMFZFFm+AyA/2Xk6EngbOCVtA1chsO1V/4oiyzcABERW7FiI6osoo2IZVQicy7HtwxRZQT8KlWaCjNm5AiOzY+Oe0jPuqdjjXjQttpWe8TMhT0Djxs/ktGRbCi07g4/kWW/C8afxX/htAc2elzyPAPIQ/Ri7cyXCbBfjXjUS9Nh2IeEnKLI8BUB+1DaI/jvXoJwfS6xC4FxOcr2i12vjpM0UWZ6dBsry/aOh61fAMfmfCyfllfoU0Y2P+dab6P/d+rVx11MCeQKALN8zDA1vAJlc+AWRpLw+D4Hcp9PHLqBEKngIkBXtdVjWWlQmA4XMgBPTymU4cONj3vXKvaXsfCgQAGkhRGfoOZDjgHwnP3F5FQXBvTp97HWUWHkDIM0Y2nY/C5zpwQw4Lq8SINC79azSdz4UEgGG7l4CnOfJDDglr09DcK/+dWkmfE7KaxIoD++aDmYtaMCDGbBtXxETQ7lXzx5dFt/8qHIGQB7eORENvI0w1E4pZAacZN+XIUDu1XPKq/MhRwDkp/Rn7+7XQY6xE6I5ZQ/BbrB+j8gWkC2g7cBeAtJFdA2GyqGIDkUYA0xAtAEYkrFstxAY7tIZY26gDJXbvYDd+5qRuM7XyBbBt+vjONgnl0NKvZtRXYewAfRtvjX8Q00cwV1JWraNRbqPRbURkTOAoxGRnHzE3KUzRpVl50MOEUAe2H88Yr0GBEu/esapHPkjWE+CPKOzh25ydVA5Sp5vHw3hbwIXInoSEvEgnY/C7Xru6MV++AIgL245FmMuQmhArQ7EvInK4zpt3Meuy3ADgDQT4tC9b6EclbbzSgOBgq5B9T7mDNuQz7c8X8kv2o9Auq8C5gB1ST5uQ/VKPW/MSl/qbmkNMbTun1G+69A2BxDma+OER12V5QqA+/c2Y1jSk5BQYSkgUGAlAb3Zr2+7W8na7fV0dH0To18G3YOwkfrOn2vjpA5f6mtpDTGk7jmUv8n4BYFLdOqEf81aXjYA5L49R2DMRtCa1A6iFBC8glgLdM7QNzM63gclaz/sR03/51DOdREld9PV9Rd65uFbM5WZ/UKQBG5DqbEnenHp6S7yuL8gkrmceHs7bT8Wi/jzoY0V2fktrSHMgGdRzgXcXKSqpya0hCzKGAHkngNfwVivJ052nM6z8TsSvALM1ssHb8l2QH1Rsn5zfzprnkf0bDshPhMyRIIuAqZBTxv3QbqyM0eAgHUbINkvu+JjJNDlhAefUbGd39Ia4kBNC3B2HpfUa+i2bstYfroIIPftn4HyQgnX1nchXKFXDM46kemrkvWb+9MRWgV6lp0Qzchp0qyY8MnaOOkNpzrSRwAL+1cqpVlC1YnFhRXd+Ws/7Mf+fs+hkc6HXOZL8XmCFfxB2nqcIoDcc+AroG9EPh61jDOI33oeCQ6gOkO/M3h9Oqf7uqTlowHUml8C03Nq49h+ShtbqDlSzxj7v8l1OUcAteanHZsT0iI1eBcJurBkZkV3/ppPBzLQ/BvKdCC3Nnayt7cGY33Psb7kCCD3HRhPN39AtIZIWYlb3yKBAhfrd+ufdHK0EiRrPh0IuhqYljZK5h8J9hHS8XrKhB3xdaZGgG6uBGq8WZRBLpHg/oru/OXUoKwCmZYxSuYfCWrpNN9OrjcBAGnGoPT8QLFoEOgGttaX7R2zomjUpw8C010NlflCIFyaXG1iBAh1nAqMdbiq5CcEuyA8W5voTnauUiS/+PgIYG5O86V8IFD9S/mPj4+Jrzt5CLggzQUFByfwBgJlgc4b8n9UsgKBuajYfeE3BAG9IL7qGADSTBD4RoarSg5OUCgEL3FV3QoqXSpHRbaR/0ncegmBpRdI3HSxJwLUdE4FRqQ5jXAuuDAILLrNAk20qEypdvbs+w7BYfz6oxOiSSYu88wkQ58h4An9p9p3qQqEl121sVcQBJgR/bcHAGFaltOI7A66hyBMWG+lKlsHeRyho2gQWDRGdw2ANDMY5egUQ/8geF7n15ft83OLLZ05qo0wz9j/xGf4BsGJ9kWnaAQIHjwdCBTtFzzGuo+qkqQP5dTGhUEQop91EkQBsLTR9WmEWwfTQaDSqlfXO96arGTp+aPfAXm/aBCIPQxE5wDHpjVMKMQTCCr2cm9WKc/k3Mb5QmDpCdADQEPazvMaAhN4mqqcFQ635NXG+UHQYFss2zuScM1nsdyUu1BJ6bF9dbjD52CfWM4mvbZ2MlWllTz/+WZgYl5t7GSfXE58XqBzsKEr0BCjJWKbuPUwEgjrqCqzVP7T3oLvkaCr35EG4h/t4jMEYdlAVZkl1oa0nec1BCINBmRiiqFTwV5AYOQdqsqscMC+OloMCNDDDcoIR0OngguDYKteO6Cy7/q5UlsrYL9tzHcIdIQhdgPIwdCp4HwhsPT3VJVVOnPyQZQ/9CTEb72GQIYbkBEZDZ0KzgcCkc0pR1tVGsnHRXlmkTLcoDIiq6FTwTlDwBaqcifFfkex/xAMN6B1rmhxKjgnCGQ7VblVW0obgx8QDDEoxoUhBUMgupeq3EnFfraA/xCY3NehOdm7gSAs+6jKpbQjbRsnpEGhEBhUxI1hQoVO9tkgMFKU9xP1DUWaqggQGGwIshoWDEGY/lTlTsqgrG2ckpcfBAaNrMf3GwKRAVTlUjrIVRun5OUMgRqQbWk7z0sILB1BVe6UcHXWVwh2GFTbHQv2GgLDWKpyKZ2QUxun5LmGoN0A7amF+ACBMp6q3Ellgr2N/g8+QdBuEGlPnbSlGHoBQQNVZZU8/ekwkFF5tbGTfSYILN1qCOvWrOvHvIFgjDTvGUZVmaWBKWk7z3sI2g1iPkgxdCrYCwhqQsdSVRbJ8UD6zvMSAsyfDJa1ydEwXp5BoI0OpVcVL5VpPfvgKwQW7xtM8H1XtHgDwdeoKq3kic9rUU5OjcQ+QdBNq9Hb2AZsLQ4EMkVu3zucqpwlwekg/QCH4dhzCNp05qi26PX51gyGXkIQoLvmG1SVThcBqW0c2/cUglaI3nVQeSODoYMzBUAgXEhVKZKWHYegnJN28h3b9woC3oTYbSdrfVGWINn7p8qtnYdTVaIOWBcD9v2SYkCAvUTfBmBA8L+AriJBYFCuoqqYpIUAcE1qR+MXBGGk36sQAUCb2Av6joNh5gqdHHQHwWVyF3VUZWvf9vNROdz1tZjYfp4QiLyrfzd4J8Q/IcSSDWloyVyhk4PZIains6M6GYTow7mWAqltHEvDWwgsa320iB4AjFntWKFTwV5AoIHjqArG77gCmJy2jWNpeAcBsja61wPAAF5D+cixQqeCC4cg/pMVKfnZrkMRWercbr5B8Dk6cn30ozEAtAkLaHF/GlEgBEL1d4Kd4ftBRwJp2s0HCJSf60zC0Y8lLtRUszL1w/gAgbZRV/MMFSz58Y4ZqFySvd08hgBJeJdhIgD38BuI/ITLLwhEFORanc8BKlTy4+3jMPIT9+3mGQSfsGn4q/G+JACgimLJY/6uQ5Ol2hSq2OcESQshCLRg4fybTPAPAovHI0N9TKlr9UM8itLhCwSit2pT8OaUOitEAsKOnf8CeiKQz5enEAi6CQd+lOxTCgB6G22gT2U8jcgHAtE7dWnopuT6KkrLd92JcKmrbyt4C4HynF405KNkl9L8Wsc8mFBAihPkCkGzNocWOddVGZLluxYDCz150ko+EIg+5OSXIwB6N++hvJRQQIoTuIWgSW8JLnWqpxIkIPLIrrtRluU1bjvZ5w7BW3rhiNec/AtmcL0ZVfvlRQpIZEftunu2QuyxZQl5ApbepLcFK/ah0PIQ/ajZ/SjCJWnbLfo/9LSbaqItDvbJtmQoW0g778r87uDrdDVE31QddUbj9uO3ceXYTizR280taQvv45KHto8jGGwBTnTVbhL/4Yh9sq2TfbJtctnKqzpr2Knp/Mz8i11LFgHhlNAT2yc19Nj7iyu68x/ecx6B4DsoibP92D6p7ebbcGBlfBlXxggAIAusxxC5jLhjyEw0N+rtZlnGQvuo5JFdh2KZO4C5jt/g4keCVTpr6Ncz+Zz9N/tB04RiP9whWyQQrq/EzpdmQvLD3dcQNh+gzI2kOnzbI+kpafgRCboQSfvO4Jjv2SIAgCxgDugKJOK9E9GGhXqHuSdrYXlKbjnYgCWXYfQIIIRar6Os0Kb+f/arzqw+NRNi8L4LMXoT6BftxGhm1KpEkcDoLTpr2JKsx+AGAABZwCzQBxCGJFW4Hax5eldgZfpP5y9pJoR2PoDId5LqBTQMrAJ9iJv6v6yJ3xHfJA/sG4lYl6DyPWBs2s4rFQTQyu7tX9arv9hJFrkGAEAWcQjd/C1qNSAEEfMu+1mlD+PLA6BkIbXUdq0BGjM2ov3/FuBZxDxLd807yde8C/bl3j3DCJizUP4B4UzQYNqZd4qPCX76DYGFcIpePOR1V8eVCwDFlCykloFdLwCnu2rEhMaQbaDrgZdB36W74z1tstfAua7/no7DEJ0CHI9YU4EpgHF9+pXiYxb/nezzgUB5UC8dco2bY7Q/UoYARDr/Vyin5dSImTvjE+Aj0M8w8jkW3QR0N4ogMhi0FiPDUGsCMAmJLNFOd53Dfb3u/XeyzwUC5T26O07SuaP341JlB4A0M5Cu7jUIUz17MUIujeimM/Kt118I9iDWCTpnaE7PZC6rR7cldD6kOdUBcDg1ynpBBIe8DOU41evm3ke8ivH0NY38F5Y5uXY+lBEA0sxADnavAaZmP9+FsoagUP8z1evs/x16xeDnyUNlAYA0M4jO8DqQqZ41YqVAYPEC9Yfmvc6i5ADIQmrpCK8GTvW8Efs8BPIG/TsviF/lm6tKOgmUhdQSDEfO80k/sUo+1UmxTWNfLhPDQv13tt9IwJyul9cX9BT2kgEgC6kloGtAG4vSiH0Lgj9BzVd17sBPKVAlGQKkmUGY8LrYM4OKEU77znCwGZjuRedDCQAQQdinT6JyClDcRuz9EGykq+urOveQnncKFaiiDwFyPeeCri5pOO2dw8F/Y8k5emXdNjxU8YcAy5pV8m9Sb4sEsIbAvmledz6UZA4gRwKlD6e9AwIFvYut9V/P5fp+LsqwKtg3daHYbaeQ12pj16tmsf8k2yeXg0O9CWWnqddf/3cizNF5h/yykMbOphIMAfo2UD4Tq3KMBOi7qHWcXlnna+dDKQBQ8yjRh0NUIUiuw0LlAbrqT9arvZvpZ1JJLgTJtSxDdHGZzK7L5exgI8b6tl5d3/PMxiKoNPcC7udGVK5HsdesVXYk6ASa2DloSrE7H0oUAWKVX8dE1FqGyLdwWm4V2yeXb1JviQSK6CosXawL6kr2Yu2yWBEk19KA0TuBcyoDAl5Dwot0ft0rlFhlAUBUch1ngd5AdEVQX4NA+A1Gm3R+7TrKRGUFQFSygKMJWPNQuRihfy+HoAt0FaLL9braFx0PuIQqSwCikvmMpsaaBzILdJKdGM2MbssWgo8RXUE3j+hib+7c+aGyBiBesogGwtZsDBcDo+3EaGaZQKC0Y1iLWC10DFyrTZG3spaxeg0AUcnfE+Cw7tNQcyZGp4JMAYIlgqAb0d+isoGgrqaj/6te/yLJb/U6AJIlN1CHhE9DZSpGjwUagJE+QdCG8D6qbxCQlwn2e1WvZ4/Xx1RM9XoAnCSLGQrdX0LNkYh1GCIjEB2GMhzRUYjU9xgnQLAdQztoO8o2hK0gH2BkE8Fgq34fz2/Hllr/D1DoAB9bI40ZAAAAAElFTkSuQmCC".into() + } +} diff --git a/src/ui/cm.rs b/src/ui/cm.rs index cce55315..a574b5e8 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 999b409e..fdb6b2df 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 0b51d8fd..e537d41f 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