mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
feat: clipboard, multi format (#8672)
* feat: clipboard, multi format Signed-off-by: fufesou <linlong1266@gmail.com> * inline Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -65,7 +65,7 @@ use crate::{
|
||||
};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::clipboard::{check_clipboard, CLIPBOARD_INTERVAL};
|
||||
use crate::clipboard::{check_clipboard, ClipboardSide, CLIPBOARD_INTERVAL};
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::ui_session_interface::SessionPermissionConfig;
|
||||
@@ -136,18 +136,11 @@ lazy_static::lazy_static! {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref ENIGO: Arc<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
|
||||
static ref OLD_CLIPBOARD_DATA: Arc<Mutex<crate::clipboard::ClipboardData>> = Default::default();
|
||||
static ref TEXT_CLIPBOARD_STATE: Arc<Mutex<TextClipboardState>> = Arc::new(Mutex::new(TextClipboardState::new()));
|
||||
}
|
||||
|
||||
const PUBLIC_SERVER: &str = "public";
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn get_old_clipboard_text() -> Arc<Mutex<crate::clipboard::ClipboardData>> {
|
||||
OLD_CLIPBOARD_DATA.clone()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn get_key_state(key: enigo::Key) -> bool {
|
||||
use enigo::KeyboardControllable;
|
||||
@@ -719,7 +712,9 @@ impl Client {
|
||||
//
|
||||
// If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn try_start_clipboard(_ctx: Option<ClientClipboardContext>) -> Option<UnboundedReceiver<()>> {
|
||||
fn try_start_clipboard(
|
||||
client_clip_ctx: Option<ClientClipboardContext>,
|
||||
) -> Option<UnboundedReceiver<()>> {
|
||||
let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap();
|
||||
if clipboard_lock.running {
|
||||
return None;
|
||||
@@ -740,15 +735,8 @@ impl Client {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(msg) = check_clipboard(&mut ctx, Some(OLD_CLIPBOARD_DATA.clone())) {
|
||||
#[cfg(feature = "flutter")]
|
||||
crate::flutter::send_text_clipboard_msg(msg);
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
if let Some(ctx) = &_ctx {
|
||||
if ctx.cfg.is_text_clipboard_required() {
|
||||
let _ = ctx.tx.send(Data::Message(msg));
|
||||
}
|
||||
}
|
||||
if let Some(msg) = check_clipboard(&mut ctx, ClipboardSide::Client) {
|
||||
Self::send_msg(&client_clip_ctx, msg);
|
||||
}
|
||||
|
||||
if !is_sent {
|
||||
@@ -765,15 +753,39 @@ impl Client {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn get_current_clipboard_msg() -> Option<Message> {
|
||||
let data = &*OLD_CLIPBOARD_DATA.lock().unwrap();
|
||||
if data.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(data.create_msg())
|
||||
fn send_msg(_ctx: &Option<ClientClipboardContext>, msg: Message) {
|
||||
crate::flutter::send_text_clipboard_msg(msg);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn send_msg(ctx: &Option<ClientClipboardContext>, msg: Message) {
|
||||
if let Some(ctx) = &ctx {
|
||||
if ctx.cfg.is_text_clipboard_required() {
|
||||
if let Some(pi) = ctx.cfg.lc.read().unwrap().peer_info.as_ref() {
|
||||
if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union {
|
||||
if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(
|
||||
&pi.version,
|
||||
&pi.platform,
|
||||
multi_clipboards,
|
||||
) {
|
||||
let _ = ctx.tx.send(Data::Message(msg_out));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = ctx.tx.send(Data::Message(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn get_current_clipboard_msg(peer_version: &str, peer_platform: &str) -> Option<Message> {
|
||||
crate::clipboard::get_cache_msg(peer_version, peer_platform)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -2023,11 +2035,16 @@ impl LoginConfigHandler {
|
||||
if display_name.is_empty() {
|
||||
display_name = crate::username();
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let my_platform = whoami::platform().to_string();
|
||||
#[cfg(target_os = "android")]
|
||||
let my_platform = "Android".into();
|
||||
let mut lr = LoginRequest {
|
||||
username: pure_id,
|
||||
password: password.into(),
|
||||
my_id,
|
||||
my_name: display_name,
|
||||
my_platform,
|
||||
option: self.get_option_message(true).into(),
|
||||
session_id: self.session_id,
|
||||
version: crate::VERSION.to_string(),
|
||||
|
||||
@@ -41,7 +41,7 @@ use crate::client::{
|
||||
new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
|
||||
};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL};
|
||||
use crate::clipboard::{update_clipboard, ClipboardSide, CLIPBOARD_INTERVAL};
|
||||
use crate::common::{get_default_sound_input, set_sound_input};
|
||||
use crate::ui_session_interface::{InvokeUiSession, Session};
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
@@ -1118,6 +1118,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
Some(login_response::Union::PeerInfo(pi)) => {
|
||||
let peer_version = pi.version.clone();
|
||||
let peer_platform = pi.platform.clone();
|
||||
self.handler.handle_peer_info(pi);
|
||||
self.check_clipboard_file_context();
|
||||
if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) {
|
||||
@@ -1139,7 +1141,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if let Some(msg_out) = Client::get_current_clipboard_msg() {
|
||||
if let Some(msg_out) =
|
||||
Client::get_current_clipboard_msg(&peer_version, &peer_platform)
|
||||
{
|
||||
let sender = self.sender.clone();
|
||||
let permission_config = self.handler.get_permission_config();
|
||||
tokio::spawn(async move {
|
||||
@@ -1180,7 +1184,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
Some(message::Union::Clipboard(cb)) => {
|
||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
update_clipboard(cb, Some(crate::client::get_old_clipboard_text()));
|
||||
update_clipboard(vec![cb], ClipboardSide::Client);
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
let content = if cb.compress {
|
||||
@@ -1194,6 +1198,12 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(message::Union::MultiClipboards(_mcb)) => {
|
||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
Some(message::Union::Cliprdr(clip)) => {
|
||||
self.handle_cliprdr_msg(clip);
|
||||
|
||||
446
src/clipboard.rs
446
src/clipboard.rs
@@ -3,24 +3,32 @@ use std::sync::{
|
||||
Arc, Mutex,
|
||||
};
|
||||
|
||||
use arboard::{ClipboardData, ClipboardFormat};
|
||||
use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
compress::{compress as compress_func, decompress},
|
||||
log,
|
||||
message_proto::*,
|
||||
ResultType,
|
||||
};
|
||||
use hbb_common::{log, message_proto::*, ResultType};
|
||||
|
||||
pub const CLIPBOARD_NAME: &'static str = "clipboard";
|
||||
pub const CLIPBOARD_INTERVAL: u64 = 333;
|
||||
const FAKE_SVG_WIDTH: usize = 999999;
|
||||
|
||||
// This format is used to store the flag in the clipboard.
|
||||
const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CONTENT: Arc<Mutex<ClipboardData>> = Default::default();
|
||||
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
|
||||
// cache the clipboard msg
|
||||
static ref LAST_MULTI_CLIPBOARDS: Arc<Mutex<MultiClipboards>> = Arc::new(Mutex::new(MultiClipboards::new()));
|
||||
}
|
||||
|
||||
const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
|
||||
ClipboardFormat::Text,
|
||||
ClipboardFormat::Html,
|
||||
ClipboardFormat::Rtf,
|
||||
ClipboardFormat::ImageRgba,
|
||||
ClipboardFormat::ImagePng,
|
||||
ClipboardFormat::ImageSvg,
|
||||
ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT),
|
||||
];
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||
static X11_CLIPBOARD: once_cell::sync::OnceCell<x11_clipboard::Clipboard> =
|
||||
once_cell::sync::OnceCell::new();
|
||||
@@ -126,58 +134,40 @@ impl ClipboardContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_clipboard(
|
||||
ctx: &mut Option<ClipboardContext>,
|
||||
old: Option<Arc<Mutex<ClipboardData>>>,
|
||||
) -> Option<Message> {
|
||||
pub fn check_clipboard(ctx: &mut Option<ClipboardContext>, side: ClipboardSide) -> Option<Message> {
|
||||
if ctx.is_none() {
|
||||
*ctx = ClipboardContext::new(true).ok();
|
||||
}
|
||||
let ctx2 = ctx.as_mut()?;
|
||||
let side = if old.is_none() { "host" } else { "client" };
|
||||
let old = if let Some(old) = old {
|
||||
old
|
||||
} else {
|
||||
CONTENT.clone()
|
||||
};
|
||||
let content = ctx2.get();
|
||||
let content = ctx2.get(side);
|
||||
if let Ok(content) = content {
|
||||
if !content.is_empty() {
|
||||
if matches!(content, ClipboardData::Text(_)) {
|
||||
// Skip the text if the last content is image-svg/html
|
||||
if ctx2.is_last_plain {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let changed = content != *old.lock().unwrap();
|
||||
if changed {
|
||||
log::info!("{} update found on {}", CLIPBOARD_NAME, side);
|
||||
let msg = content.create_msg();
|
||||
*old.lock().unwrap() = content;
|
||||
return Some(msg);
|
||||
}
|
||||
let mut msg = Message::new();
|
||||
let clipboards = proto::create_multi_clipboards(content);
|
||||
msg.set_multi_clipboards(clipboards.clone());
|
||||
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
|
||||
return Some(msg);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn update_clipboard_(clipboard: Clipboard, old: Option<Arc<Mutex<ClipboardData>>>) {
|
||||
let content = ClipboardData::from_msg(clipboard);
|
||||
if content.is_empty() {
|
||||
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
let mut to_update_data = proto::from_multi_clipbards(multi_clipboards);
|
||||
if to_update_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
match ClipboardContext::new(false) {
|
||||
Ok(mut ctx) => {
|
||||
let side = if old.is_none() { "host" } else { "client" };
|
||||
let old = if let Some(old) = old {
|
||||
old
|
||||
to_update_data.push(ClipboardData::Special((
|
||||
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
|
||||
side.get_owner_data(),
|
||||
)));
|
||||
if let Err(e) = ctx.set(&to_update_data) {
|
||||
log::debug!("Failed to set clipboard: {}", e);
|
||||
} else {
|
||||
CONTENT.clone()
|
||||
};
|
||||
allow_err!(ctx.set(&content));
|
||||
*old.lock().unwrap() = content;
|
||||
log::debug!("{} updated on {}", CLIPBOARD_NAME, side);
|
||||
log::debug!("{} updated on {}", CLIPBOARD_NAME, side);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to create clipboard context: {}", err);
|
||||
@@ -185,137 +175,17 @@ fn update_clipboard_(clipboard: Clipboard, old: Option<Arc<Mutex<ClipboardData>>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_clipboard(clipboard: Clipboard, old: Option<Arc<Mutex<ClipboardData>>>) {
|
||||
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
std::thread::spawn(move || {
|
||||
update_clipboard_(clipboard, old);
|
||||
update_clipboard_(multi_clipboards, side);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ClipboardData {
|
||||
Text(String),
|
||||
Image(arboard::ImageData<'static>, u64),
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl Default for ClipboardData {
|
||||
fn default() -> Self {
|
||||
ClipboardData::Empty
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipboardData {
|
||||
fn image(image: arboard::ImageData<'static>) -> ClipboardData {
|
||||
let hash = 0;
|
||||
/*
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
let mut hasher = DefaultHasher::new();
|
||||
image.bytes.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
*/
|
||||
ClipboardData::Image(image, hash)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
ClipboardData::Empty => true,
|
||||
ClipboardData::Text(s) => s.is_empty(),
|
||||
ClipboardData::Image(a, _) => a.bytes().is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_msg(clipboard: Clipboard) -> Self {
|
||||
let is_image = clipboard.width > 0;
|
||||
let data = if clipboard.compress {
|
||||
decompress(&clipboard.content)
|
||||
} else {
|
||||
clipboard.content.into()
|
||||
};
|
||||
if is_image {
|
||||
// We cannot use data.start_with(b"<svg") to check if it is svg image
|
||||
// because svg image may starts with other bytes
|
||||
let img = if clipboard.height == 0 && clipboard.width as usize == FAKE_SVG_WIDTH {
|
||||
arboard::ImageData::svg(std::str::from_utf8(&data).unwrap_or_default())
|
||||
} else {
|
||||
arboard::ImageData::rgba(clipboard.width as _, clipboard.height as _, data.into())
|
||||
};
|
||||
ClipboardData::Image(img, 0)
|
||||
} else {
|
||||
if let Ok(content) = String::from_utf8(data) {
|
||||
ClipboardData::Text(content)
|
||||
} else {
|
||||
ClipboardData::Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_msg(&self) -> Message {
|
||||
let mut msg = Message::new();
|
||||
|
||||
match self {
|
||||
ClipboardData::Text(s) => {
|
||||
let compressed = compress_func(s.as_bytes());
|
||||
let compress = compressed.len() < s.as_bytes().len();
|
||||
let content = if compress {
|
||||
compressed
|
||||
} else {
|
||||
s.clone().into_bytes()
|
||||
};
|
||||
msg.set_clipboard(Clipboard {
|
||||
compress,
|
||||
content: content.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
ClipboardData::Image(a, _) => {
|
||||
let compressed = compress_func(&a.bytes());
|
||||
let compress = compressed.len() < a.bytes().len();
|
||||
let content = if compress {
|
||||
compressed
|
||||
} else {
|
||||
a.bytes().to_vec()
|
||||
};
|
||||
let (w, h) = match a {
|
||||
arboard::ImageData::Rgba(a) => (a.width, a.height),
|
||||
arboard::ImageData::Svg(_) => (FAKE_SVG_WIDTH as _, 0 as _),
|
||||
};
|
||||
msg.set_clipboard(Clipboard {
|
||||
compress,
|
||||
content: content.into(),
|
||||
width: w as _,
|
||||
height: h as _,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
msg
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ClipboardData {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(ClipboardData::Text(a), ClipboardData::Text(b)) => a == b,
|
||||
(ClipboardData::Image(a, _), ClipboardData::Image(b, _)) => match (a, b) {
|
||||
(arboard::ImageData::Rgba(a), arboard::ImageData::Rgba(b)) => {
|
||||
a.width == b.width && a.height == b.height && a.bytes == b.bytes
|
||||
}
|
||||
(arboard::ImageData::Svg(a), arboard::ImageData::Svg(b)) => a == b,
|
||||
_ => false,
|
||||
},
|
||||
(ClipboardData::Empty, ClipboardData::Empty) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
|
||||
pub struct ClipboardContext {
|
||||
inner: arboard::Clipboard,
|
||||
counter: (Arc<AtomicU64>, u64),
|
||||
shutdown: Option<Shutdown>,
|
||||
is_last_plain: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
|
||||
@@ -392,7 +262,6 @@ impl ClipboardContext {
|
||||
inner: board,
|
||||
counter: (change_count, 0),
|
||||
shutdown,
|
||||
is_last_plain: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -402,35 +271,29 @@ impl ClipboardContext {
|
||||
self.counter.0.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn get(&mut self) -> ResultType<ClipboardData> {
|
||||
pub fn get(&mut self, side: ClipboardSide) -> ResultType<Vec<ClipboardData>> {
|
||||
let cn = self.change_count();
|
||||
let _lock = ARBOARD_MTX.lock().unwrap();
|
||||
// only for image for the time being,
|
||||
// because I do not want to change behavior of text clipboard for the time being
|
||||
if cn != self.counter.1 {
|
||||
self.is_last_plain = false;
|
||||
self.counter.1 = cn;
|
||||
if let Ok(image) = self.inner.get_image() {
|
||||
// Both text and image svg may be set by some applications
|
||||
// But we only want to send the svg content.
|
||||
//
|
||||
// We can't call `get_text()` and store current text in `old` in outer scope,
|
||||
// because it may be updated later than svg.
|
||||
// Then the text will still be sent and replace the image svg content.
|
||||
self.is_last_plain = matches!(image, arboard::ImageData::Svg(_));
|
||||
return Ok(ClipboardData::image(image));
|
||||
let data = self.inner.get_formats(SUPPORTED_FORMATS)?;
|
||||
if !data.is_empty() {
|
||||
for c in data.iter() {
|
||||
if let ClipboardData::Special((_, d)) = c {
|
||||
if side.is_owner(d) {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
Ok(ClipboardData::Text(self.inner.get_text()?))
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn set(&mut self, data: &ClipboardData) -> ResultType<()> {
|
||||
fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> {
|
||||
let _lock = ARBOARD_MTX.lock().unwrap();
|
||||
match data {
|
||||
ClipboardData::Text(s) => self.inner.set_text(s)?,
|
||||
ClipboardData::Image(a, _) => self.inner.set_image(a.clone())?,
|
||||
_ => {}
|
||||
}
|
||||
self.inner.set_formats(data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -442,3 +305,210 @@ impl Drop for ClipboardContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool {
|
||||
use hbb_common::get_version_number;
|
||||
get_version_number(peer_version) >= get_version_number("1.2.7")
|
||||
&& !["", "Android", &whoami::Platform::Ios.to_string()].contains(&peer_platform)
|
||||
}
|
||||
|
||||
pub fn get_cache_msg(peer_version: &str, peer_platform: &str) -> Option<Message> {
|
||||
let multi_clipboards = LAST_MULTI_CLIPBOARDS.lock().unwrap().clone();
|
||||
if multi_clipboards.clipboards.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut msg = Message::new();
|
||||
if is_support_multi_clipboard(peer_version, peer_platform) {
|
||||
msg.set_multi_clipboards(multi_clipboards);
|
||||
} else {
|
||||
for clipboard in multi_clipboards.clipboards.iter() {
|
||||
if clipboard.format.enum_value() == Ok(hbb_common::message_proto::ClipboardFormat::Text)
|
||||
{
|
||||
msg.set_clipboard(clipboard.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(msg)
|
||||
}
|
||||
|
||||
pub fn reset_cache() {
|
||||
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = MultiClipboards::new();
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ClipboardSide {
|
||||
Host,
|
||||
Client,
|
||||
}
|
||||
|
||||
impl ClipboardSide {
|
||||
// 01: the clipboard is owned by the host
|
||||
// 10: the clipboard is owned by the client
|
||||
fn get_owner_data(&self) -> Vec<u8> {
|
||||
match self {
|
||||
ClipboardSide::Host => vec![0b01],
|
||||
ClipboardSide::Client => vec![0b10],
|
||||
}
|
||||
}
|
||||
|
||||
fn is_owner(&self, data: &[u8]) -> bool {
|
||||
if data.len() == 0 {
|
||||
return false;
|
||||
}
|
||||
match self {
|
||||
ClipboardSide::Host => data[0] & 0b01 == 0b01,
|
||||
ClipboardSide::Client => data[0] & 0b10 == 0b10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ClipboardSide {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ClipboardSide::Host => write!(f, "host"),
|
||||
ClipboardSide::Client => write!(f, "client"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use proto::get_msg_if_not_support_multi_clip;
|
||||
mod proto {
|
||||
use arboard::ClipboardData;
|
||||
use hbb_common::{
|
||||
compress::{compress as compress_func, decompress},
|
||||
message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards},
|
||||
};
|
||||
|
||||
fn plain_to_proto(s: String, format: ClipboardFormat) -> Clipboard {
|
||||
let compressed = compress_func(s.as_bytes());
|
||||
let compress = compressed.len() < s.as_bytes().len();
|
||||
let content = if compress {
|
||||
compressed
|
||||
} else {
|
||||
s.bytes().collect::<Vec<u8>>()
|
||||
};
|
||||
Clipboard {
|
||||
compress,
|
||||
content: content.into(),
|
||||
format: format.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn image_to_proto(a: arboard::ImageData) -> Clipboard {
|
||||
match &a {
|
||||
arboard::ImageData::Rgba(rgba) => {
|
||||
let compressed = compress_func(&a.bytes());
|
||||
let compress = compressed.len() < a.bytes().len();
|
||||
let content = if compress {
|
||||
compressed
|
||||
} else {
|
||||
a.bytes().to_vec()
|
||||
};
|
||||
Clipboard {
|
||||
compress,
|
||||
content: content.into(),
|
||||
width: rgba.width as _,
|
||||
height: rgba.height as _,
|
||||
format: ClipboardFormat::ImageRgba.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
arboard::ImageData::Png(png) => Clipboard {
|
||||
compress: false,
|
||||
content: png.to_owned().to_vec().into(),
|
||||
format: ClipboardFormat::ImagePng.into(),
|
||||
..Default::default()
|
||||
},
|
||||
arboard::ImageData::Svg(_) => {
|
||||
let compressed = compress_func(&a.bytes());
|
||||
let compress = compressed.len() < a.bytes().len();
|
||||
let content = if compress {
|
||||
compressed
|
||||
} else {
|
||||
a.bytes().to_vec()
|
||||
};
|
||||
Clipboard {
|
||||
compress,
|
||||
content: content.into(),
|
||||
format: ClipboardFormat::ImageSvg.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clipboard_data_to_proto(data: ClipboardData) -> Option<Clipboard> {
|
||||
let d = match data {
|
||||
ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text),
|
||||
ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf),
|
||||
ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html),
|
||||
ClipboardData::Image(a) => image_to_proto(a),
|
||||
_ => return None,
|
||||
};
|
||||
Some(d)
|
||||
}
|
||||
|
||||
pub fn create_multi_clipboards(vec_data: Vec<ClipboardData>) -> MultiClipboards {
|
||||
MultiClipboards {
|
||||
clipboards: vec_data
|
||||
.into_iter()
|
||||
.filter_map(clipboard_data_to_proto)
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn from_clipboard(clipboard: Clipboard) -> Option<ClipboardData> {
|
||||
let data = if clipboard.compress {
|
||||
decompress(&clipboard.content)
|
||||
} else {
|
||||
clipboard.content.into()
|
||||
};
|
||||
match clipboard.format.enum_value() {
|
||||
Ok(ClipboardFormat::Text) => String::from_utf8(data).ok().map(ClipboardData::Text),
|
||||
Ok(ClipboardFormat::Rtf) => String::from_utf8(data).ok().map(ClipboardData::Rtf),
|
||||
Ok(ClipboardFormat::Html) => String::from_utf8(data).ok().map(ClipboardData::Html),
|
||||
Ok(ClipboardFormat::ImageRgba) => Some(ClipboardData::Image(arboard::ImageData::rgba(
|
||||
clipboard.width as _,
|
||||
clipboard.height as _,
|
||||
data.into(),
|
||||
))),
|
||||
Ok(ClipboardFormat::ImagePng) => {
|
||||
Some(ClipboardData::Image(arboard::ImageData::png(data.into())))
|
||||
}
|
||||
Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg(
|
||||
std::str::from_utf8(&data).unwrap_or_default(),
|
||||
))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_multi_clipbards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
|
||||
multi_clipboards
|
||||
.into_iter()
|
||||
.filter_map(from_clipboard)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_msg_if_not_support_multi_clip(
|
||||
version: &str,
|
||||
platform: &str,
|
||||
multi_clipboards: &MultiClipboards,
|
||||
) -> Option<Message> {
|
||||
if crate::clipboard::is_support_multi_clipboard(version, platform) {
|
||||
return None;
|
||||
}
|
||||
let mut msg = Message::new();
|
||||
// Find the first text clipboard and send it.
|
||||
for clipboard in multi_clipboards.clipboards.iter() {
|
||||
if clipboard.format.enum_value() == Ok(ClipboardFormat::Text) {
|
||||
msg.set_clipboard(clipboard.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1256,6 +1256,19 @@ pub fn update_text_clipboard_required() {
|
||||
pub fn send_text_clipboard_msg(msg: Message) {
|
||||
for s in sessions::get_sessions() {
|
||||
if s.is_text_clipboard_required() {
|
||||
// Check if the client supports multi clipboards
|
||||
if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union {
|
||||
let version = s.ui_handler.peer_info.read().unwrap().version.clone();
|
||||
let platform = s.ui_handler.peer_info.read().unwrap().platform.clone();
|
||||
if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(
|
||||
&version,
|
||||
&platform,
|
||||
multi_clipboards,
|
||||
) {
|
||||
s.send(Data::Message(msg_out));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
s.send(Data::Message(msg.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
pub use crate::clipboard::{
|
||||
check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME,
|
||||
CONTENT,
|
||||
check_clipboard, get_cache_msg, ClipboardContext, ClipboardSide,
|
||||
CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -11,7 +11,7 @@ struct State {
|
||||
|
||||
impl super::service::Reset for State {
|
||||
fn reset(&mut self) {
|
||||
*CONTENT.lock().unwrap() = Default::default();
|
||||
crate::clipboard::reset_cache();
|
||||
self.ctx = None;
|
||||
}
|
||||
|
||||
@@ -34,14 +34,14 @@ pub fn new() -> GenericService {
|
||||
}
|
||||
|
||||
fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> {
|
||||
if let Some(msg) = check_clipboard(&mut state.ctx, None) {
|
||||
if let Some(msg) = check_clipboard(&mut state.ctx, ClipboardSide::Host) {
|
||||
sp.send(msg);
|
||||
}
|
||||
sp.snapshot(|sps| {
|
||||
let data = CONTENT.lock().unwrap().clone();
|
||||
if !data.is_empty() {
|
||||
let msg_out = data.create_msg();
|
||||
sps.send_shared(Arc::new(msg_out));
|
||||
// Just create a message with multi clipboards here
|
||||
// The actual peer version and peer platform will be checked again before sending.
|
||||
if let Some(msg) = get_cache_msg("1.2.7", "Windows") {
|
||||
sps.send_shared(Arc::new(msg));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{input_service::*, *};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::clipboard::update_clipboard;
|
||||
use crate::clipboard::{update_clipboard, ClipboardSide};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
use crate::clipboard_file::*;
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -685,8 +685,19 @@ impl Connection {
|
||||
msg = Arc::new(new_msg);
|
||||
}
|
||||
}
|
||||
Some(message::Union::MultiClipboards(_multi_clipboards)) => {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(&conn.lr.version, &conn.lr.my_platform, _multi_clipboards) {
|
||||
if let Err(err) = conn.stream.send(&msg_out).await {
|
||||
conn.on_close(&err.to_string(), false).await;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let msg: &Message = &msg;
|
||||
if let Err(err) = conn.stream.send(msg).await {
|
||||
conn.on_close(&err.to_string(), false).await;
|
||||
@@ -2053,7 +2064,14 @@ impl Connection {
|
||||
{
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if self.clipboard {
|
||||
update_clipboard(_cb, None);
|
||||
update_clipboard(vec![_cb], ClipboardSide::Host);
|
||||
}
|
||||
}
|
||||
Some(message::Union::MultiClipboards(_mcb)) =>
|
||||
{
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if self.clipboard {
|
||||
update_clipboard(_mcb.clipboards, ClipboardSide::Host);
|
||||
}
|
||||
}
|
||||
Some(message::Union::Cliprdr(_clip)) =>
|
||||
|
||||
Reference in New Issue
Block a user