From 0b72b83241f73267c661eb8ccc82862b6aca795f Mon Sep 17 00:00:00 2001 From: Dr1mH4X <102709153+Dr1mH4X@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:57:20 +0800 Subject: [PATCH 1/5] feat: Add timestamps to clipboard updates --- src-tauri/src/runtime/clipboard.rs | 13 +++++++++++++ src-tauri/src/runtime/lan/discovery.rs | 14 +++++++++----- src-tauri/src/runtime/lan/peer.rs | 4 +++- src-tauri/src/runtime/lan/protocol.rs | 1 + src-tauri/src/runtime/messages.rs | 2 ++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/runtime/clipboard.rs b/src-tauri/src/runtime/clipboard.rs index 918147a..abe343b 100644 --- a/src-tauri/src/runtime/clipboard.rs +++ b/src-tauri/src/runtime/clipboard.rs @@ -87,6 +87,10 @@ pub async fn start_clipboard_monitor( content_type: CONTENT_TYPE_TEXT.to_string(), data: text, sender_device_id: device_id.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), }, }; let _ = tx.send(update); @@ -142,6 +146,10 @@ pub async fn start_clipboard_monitor( content_type: CONTENT_TYPE_IMAGE_PNG.to_string(), data: b64, sender_device_id: device_id.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), }, }; let _ = tx.send(update); @@ -185,11 +193,16 @@ pub async fn start_clipboard_setter( events: mpsc::Sender, cancel: CancellationToken, ) { + let mut last_timestamp = 0; loop { tokio::select! { _ = cancel.cancelled() => break, maybe_payload = rx.recv() => { if let Some(payload) = maybe_payload { + if payload.timestamp < last_timestamp { + continue; + } + last_timestamp = payload.timestamp; match payload.content_type.as_str() { CONTENT_TYPE_TEXT => { if let Err(err) = set_text(&payload.data, disable_flag.clone()).await { diff --git a/src-tauri/src/runtime/lan/discovery.rs b/src-tauri/src/runtime/lan/discovery.rs index 946eee8..d70db95 100644 --- a/src-tauri/src/runtime/lan/discovery.rs +++ b/src-tauri/src/runtime/lan/discovery.rs @@ -85,10 +85,7 @@ pub async fn run_beacon_broadcaster( discovery_port }; - // Bind to INADDR_ANY so the OS picks the right interface. - // We use port 0 for the *sending* socket so we don't conflict with the - // listener that is bound to the same discovery port. - let socket = match UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)).await { + let mut socket = match UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)).await { Ok(s) => s, Err(e) => { let _ = events @@ -141,9 +138,16 @@ pub async fn run_beacon_broadcaster( let _ = events .send(RuntimeEvent::Log(RuntimeLogEvent::new( Level::Warn, - format!("LAN beacon send failed: {}", e), + format!("LAN beacon send failed (attempting rebind): {}", e), ))) .await; + + // Attempt to rebind on network error + if let Ok(new_socket) = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)).await { + if new_socket.set_broadcast(true).is_ok() { + socket = new_socket; + } + } } seq = seq.wrapping_add(1); } diff --git a/src-tauri/src/runtime/lan/peer.rs b/src-tauri/src/runtime/lan/peer.rs index ef08ce6..a1b4579 100644 --- a/src-tauri/src/runtime/lan/peer.rs +++ b/src-tauri/src/runtime/lan/peer.rs @@ -441,6 +441,7 @@ async fn run_peer_session( let msg = PeerMessage::Clipboard { content_type: update.payload.content_type.clone(), data: update.payload.data.clone(), + timestamp: update.payload.timestamp, }; let frame = encode_peer_message(&msg); let mut w = writer.lock().await; @@ -490,10 +491,11 @@ async fn run_peer_session( PeerMessage::Pong { .. } => { last_pong = Instant::now(); } - PeerMessage::Clipboard { content_type, data } => { + PeerMessage::Clipboard { content_type, data, timestamp } => { let payload = ClipboardBroadcastPayload { content_type: content_type.clone(), data, + timestamp, }; let _ = tx_in.send(payload).await; diff --git a/src-tauri/src/runtime/lan/protocol.rs b/src-tauri/src/runtime/lan/protocol.rs index 1ed8c40..520ff47 100644 --- a/src-tauri/src/runtime/lan/protocol.rs +++ b/src-tauri/src/runtime/lan/protocol.rs @@ -100,6 +100,7 @@ pub enum PeerMessage { Clipboard { content_type: String, data: String, + timestamp: u64, }, } diff --git a/src-tauri/src/runtime/messages.rs b/src-tauri/src/runtime/messages.rs index 8d63930..853ac31 100644 --- a/src-tauri/src/runtime/messages.rs +++ b/src-tauri/src/runtime/messages.rs @@ -39,6 +39,7 @@ pub struct ClipboardUpdatePayload { pub content_type: String, pub data: String, pub sender_device_id: String, + pub timestamp: u64, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -67,6 +68,7 @@ pub struct ClipboardBroadcast { pub struct ClipboardBroadcastPayload { pub content_type: String, pub data: String, + pub timestamp: u64, } // 客户端发送的消息结构 From 55c424fc729d9c23961d74818a846dbf54af24d2 Mon Sep 17 00:00:00 2001 From: Dr1mH4X <102709153+Dr1mH4X@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:08:04 +0800 Subject: [PATCH 2/5] fix: Improve clipboard dedupe and LAN beacon logging --- src-tauri/src/runtime/clipboard.rs | 19 ++++++++++------- src-tauri/src/runtime/lan/discovery.rs | 29 +++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/runtime/clipboard.rs b/src-tauri/src/runtime/clipboard.rs index abe343b..0fca629 100644 --- a/src-tauri/src/runtime/clipboard.rs +++ b/src-tauri/src/runtime/clipboard.rs @@ -89,7 +89,7 @@ pub async fn start_clipboard_monitor( sender_device_id: device_id.clone(), timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_secs(), }, }; @@ -148,7 +148,7 @@ pub async fn start_clipboard_monitor( sender_device_id: device_id.clone(), timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_secs(), }, }; @@ -193,18 +193,19 @@ pub async fn start_clipboard_setter( events: mpsc::Sender, cancel: CancellationToken, ) { - let mut last_timestamp = 0; + let mut last_text = String::new(); + let mut last_image_data = String::new(); loop { tokio::select! { _ = cancel.cancelled() => break, maybe_payload = rx.recv() => { if let Some(payload) = maybe_payload { - if payload.timestamp < last_timestamp { - continue; - } - last_timestamp = payload.timestamp; match payload.content_type.as_str() { CONTENT_TYPE_TEXT => { + if payload.data == last_text { + continue; + } + last_text = payload.data.clone(); if let Err(err) = set_text(&payload.data, disable_flag.clone()).await { let _ = events.send(RuntimeEvent::Log(RuntimeLogEvent::new(Level::Error, format!("设置文本剪贴板失败: {}", err)))).await; } else { @@ -213,6 +214,10 @@ pub async fn start_clipboard_setter( } } CONTENT_TYPE_IMAGE_PNG => { + if payload.data == last_image_data { + continue; + } + last_image_data = payload.data.clone(); if let Err(err) = set_image_from_base64(&payload.data, disable_flag.clone()).await { let _ = events.send(RuntimeEvent::Log(RuntimeLogEvent::new(Level::Error, format!("设置图片剪贴板失败: {}", err)))).await; } else { diff --git a/src-tauri/src/runtime/lan/discovery.rs b/src-tauri/src/runtime/lan/discovery.rs index d70db95..dd8cd7c 100644 --- a/src-tauri/src/runtime/lan/discovery.rs +++ b/src-tauri/src/runtime/lan/discovery.rs @@ -143,9 +143,32 @@ pub async fn run_beacon_broadcaster( .await; // Attempt to rebind on network error - if let Ok(new_socket) = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)).await { - if new_socket.set_broadcast(true).is_ok() { - socket = new_socket; + match UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)).await { + Ok(new_socket) => { + if let Err(err) = new_socket.set_broadcast(true) { + let _ = events + .send(RuntimeEvent::Log(RuntimeLogEvent::new( + Level::Error, + format!("LAN beacon rebind set_broadcast failed: {}", err), + ))) + .await; + } else { + socket = new_socket; + let _ = events + .send(RuntimeEvent::Log(RuntimeLogEvent::new( + Level::Info, + "LAN beacon rebind successful".to_string(), + ))) + .await; + } + } + Err(err) => { + let _ = events + .send(RuntimeEvent::Log(RuntimeLogEvent::new( + Level::Error, + format!("LAN beacon rebind failed: {}", err), + ))) + .await; } } } From 520e31a77cbf3db725204a1d74a3b77baf832b05 Mon Sep 17 00:00:00 2001 From: Dr1mH4X <102709153+Dr1mH4X@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:14:55 +0800 Subject: [PATCH 3/5] fix: Prevent redundant clipboard sets using timestamps --- src-tauri/src/runtime/clipboard.rs | 33 +++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/runtime/clipboard.rs b/src-tauri/src/runtime/clipboard.rs index 0fca629..6ae5c55 100644 --- a/src-tauri/src/runtime/clipboard.rs +++ b/src-tauri/src/runtime/clipboard.rs @@ -87,10 +87,7 @@ pub async fn start_clipboard_monitor( content_type: CONTENT_TYPE_TEXT.to_string(), data: text, sender_device_id: device_id.clone(), - timestamp: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(), + timestamp: current_timestamp(), }, }; let _ = tx.send(update); @@ -146,10 +143,7 @@ pub async fn start_clipboard_monitor( content_type: CONTENT_TYPE_IMAGE_PNG.to_string(), data: b64, sender_device_id: device_id.clone(), - timestamp: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(), + timestamp: current_timestamp(), }, }; let _ = tx.send(update); @@ -193,8 +187,10 @@ pub async fn start_clipboard_setter( events: mpsc::Sender, cancel: CancellationToken, ) { - let mut last_text = String::new(); - let mut last_image_data = String::new(); + let mut last_text: Option = None; + let mut last_text_ts = 0; + let mut last_image_data: Option = None; + let mut last_image_ts = 0; loop { tokio::select! { _ = cancel.cancelled() => break, @@ -202,10 +198,11 @@ pub async fn start_clipboard_setter( if let Some(payload) = maybe_payload { match payload.content_type.as_str() { CONTENT_TYPE_TEXT => { - if payload.data == last_text { + if payload.timestamp <= last_text_ts || Some(&payload.data) == last_text.as_ref() { continue; } - last_text = payload.data.clone(); + last_text_ts = payload.timestamp; + last_text = Some(payload.data.clone()); if let Err(err) = set_text(&payload.data, disable_flag.clone()).await { let _ = events.send(RuntimeEvent::Log(RuntimeLogEvent::new(Level::Error, format!("设置文本剪贴板失败: {}", err)))).await; } else { @@ -214,10 +211,11 @@ pub async fn start_clipboard_setter( } } CONTENT_TYPE_IMAGE_PNG => { - if payload.data == last_image_data { + if payload.timestamp <= last_image_ts || Some(&payload.data) == last_image_data.as_ref() { continue; } - last_image_data = payload.data.clone(); + last_image_ts = payload.timestamp; + last_image_data = Some(payload.data.clone()); if let Err(err) = set_image_from_base64(&payload.data, disable_flag.clone()).await { let _ = events.send(RuntimeEvent::Log(RuntimeLogEvent::new(Level::Error, format!("设置图片剪贴板失败: {}", err)))).await; } else { @@ -309,6 +307,13 @@ fn read_clipboard_content() -> Result { } } +fn current_timestamp() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + fn encode_png(bytes: Vec, width: u32, height: u32) -> Result, String> { if let Some(rgba) = RgbaImage::from_raw(width, height, bytes) { let mut cursor = Cursor::new(Vec::new()); From e70d81a2aeed792dd7e3a34326d73ebb1fcc37e5 Mon Sep 17 00:00:00 2001 From: Dr1mH4X <102709153+Dr1mH4X@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:21:39 +0800 Subject: [PATCH 4/5] fix: timestamp to u64 --- src-tauri/src/runtime/clipboard.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/runtime/clipboard.rs b/src-tauri/src/runtime/clipboard.rs index 6ae5c55..b7ea575 100644 --- a/src-tauri/src/runtime/clipboard.rs +++ b/src-tauri/src/runtime/clipboard.rs @@ -188,9 +188,7 @@ pub async fn start_clipboard_setter( cancel: CancellationToken, ) { let mut last_text: Option = None; - let mut last_text_ts = 0; let mut last_image_data: Option = None; - let mut last_image_ts = 0; loop { tokio::select! { _ = cancel.cancelled() => break, @@ -198,10 +196,9 @@ pub async fn start_clipboard_setter( if let Some(payload) = maybe_payload { match payload.content_type.as_str() { CONTENT_TYPE_TEXT => { - if payload.timestamp <= last_text_ts || Some(&payload.data) == last_text.as_ref() { + if Some(&payload.data) == last_text.as_ref() { continue; } - last_text_ts = payload.timestamp; last_text = Some(payload.data.clone()); if let Err(err) = set_text(&payload.data, disable_flag.clone()).await { let _ = events.send(RuntimeEvent::Log(RuntimeLogEvent::new(Level::Error, format!("设置文本剪贴板失败: {}", err)))).await; @@ -211,10 +208,9 @@ pub async fn start_clipboard_setter( } } CONTENT_TYPE_IMAGE_PNG => { - if payload.timestamp <= last_image_ts || Some(&payload.data) == last_image_data.as_ref() { + if Some(&payload.data) == last_image_data.as_ref() { continue; } - last_image_ts = payload.timestamp; last_image_data = Some(payload.data.clone()); if let Err(err) = set_image_from_base64(&payload.data, disable_flag.clone()).await { let _ = events.send(RuntimeEvent::Log(RuntimeLogEvent::new(Level::Error, format!("设置图片剪贴板失败: {}", err)))).await; @@ -311,7 +307,7 @@ fn current_timestamp() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() - .as_secs() + .as_millis() as u64 } fn encode_png(bytes: Vec, width: u32, height: u32) -> Result, String> { From 60a45839b36411b6ca58b79691e0a367f209d017 Mon Sep 17 00:00:00 2001 From: Dr1mH4X <102709153+Dr1mH4X@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:29:00 +0800 Subject: [PATCH 5/5] fix: Ignore stale clipboard payloads --- src-tauri/src/runtime/clipboard.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/runtime/clipboard.rs b/src-tauri/src/runtime/clipboard.rs index b7ea575..3de0655 100644 --- a/src-tauri/src/runtime/clipboard.rs +++ b/src-tauri/src/runtime/clipboard.rs @@ -188,7 +188,9 @@ pub async fn start_clipboard_setter( cancel: CancellationToken, ) { let mut last_text: Option = None; + let mut last_text_ts = 0; let mut last_image_data: Option = None; + let mut last_image_ts = 0; loop { tokio::select! { _ = cancel.cancelled() => break, @@ -196,9 +198,10 @@ pub async fn start_clipboard_setter( if let Some(payload) = maybe_payload { match payload.content_type.as_str() { CONTENT_TYPE_TEXT => { - if Some(&payload.data) == last_text.as_ref() { + if payload.timestamp <= last_text_ts || Some(&payload.data) == last_text.as_ref() { continue; } + last_text_ts = payload.timestamp; last_text = Some(payload.data.clone()); if let Err(err) = set_text(&payload.data, disable_flag.clone()).await { let _ = events.send(RuntimeEvent::Log(RuntimeLogEvent::new(Level::Error, format!("设置文本剪贴板失败: {}", err)))).await; @@ -208,9 +211,10 @@ pub async fn start_clipboard_setter( } } CONTENT_TYPE_IMAGE_PNG => { - if Some(&payload.data) == last_image_data.as_ref() { + if payload.timestamp <= last_image_ts || Some(&payload.data) == last_image_data.as_ref() { continue; } + last_image_ts = payload.timestamp; last_image_data = Some(payload.data.clone()); if let Err(err) = set_image_from_base64(&payload.data, disable_flag.clone()).await { let _ = events.send(RuntimeEvent::Log(RuntimeLogEvent::new(Level::Error, format!("设置图片剪贴板失败: {}", err)))).await;