托盘
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
//! 剪贴板监听线程:基于 GetClipboardSequenceNumber 轮询
|
||||
//!
|
||||
//! 选用轮询而非 AddClipboardFormatListener 消息窗口:实现更简单、无需消息循环,
|
||||
//! 800ms 间隔对剪贴板场景延迟可接受,且 GetClipboardSequenceNumber 不需要 OpenClipboard,开销极小。
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use super::reader::{read_clipboard, ClipData};
|
||||
use super::storage::{NewItem, Storage};
|
||||
use windows_sys::Win32::System::DataExchange::GetClipboardSequenceNumber;
|
||||
|
||||
/// 启动监听线程,返回 JoinHandle。
|
||||
pub fn start_monitor(
|
||||
storage: Arc<Storage>,
|
||||
app: AppHandle,
|
||||
settings: Arc<Mutex<super::manager::ClipboardSettings>>,
|
||||
suppress: Arc<AtomicBool>,
|
||||
stop: Arc<AtomicBool>,
|
||||
) -> thread::JoinHandle<()> {
|
||||
thread::spawn(move || loop {
|
||||
if stop.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(800));
|
||||
if stop.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let seq = unsafe { GetClipboardSequenceNumber() };
|
||||
// 首次记录基准;后续对比变化
|
||||
let last = LAST_SEQ.with(|c| {
|
||||
let prev = c.get().unwrap_or(seq);
|
||||
c.set(Some(seq));
|
||||
prev
|
||||
});
|
||||
if seq == last {
|
||||
continue;
|
||||
}
|
||||
// 序列号变化,处理一次
|
||||
if suppress.swap(false, Ordering::SeqCst) {
|
||||
// 由本应用 copy_back 触发,跳过记录
|
||||
continue;
|
||||
}
|
||||
let (rec_text, rec_image, rec_files, max_items, max_image_kb, dedup) = {
|
||||
let s = settings.lock().unwrap();
|
||||
(
|
||||
s.record_text,
|
||||
s.record_image,
|
||||
s.record_files,
|
||||
s.max_items,
|
||||
s.max_image_kb,
|
||||
s.dedup,
|
||||
)
|
||||
};
|
||||
let data = match read_clipboard() {
|
||||
Some(d) => d,
|
||||
None => continue,
|
||||
};
|
||||
let item = match &data {
|
||||
ClipData::Text(t) => {
|
||||
if !rec_text {
|
||||
continue;
|
||||
}
|
||||
build_text_item(t)
|
||||
}
|
||||
ClipData::Image { dib, width, height } => {
|
||||
if !rec_image {
|
||||
continue;
|
||||
}
|
||||
if max_image_kb > 0 && (dib.len() as u64 / 1024) > max_image_kb {
|
||||
continue;
|
||||
}
|
||||
build_image_item(dib, *width, *height)
|
||||
}
|
||||
ClipData::Files(fs) => {
|
||||
if !rec_files {
|
||||
continue;
|
||||
}
|
||||
build_files_item(fs)
|
||||
}
|
||||
};
|
||||
if let Some(_id) = storage.insert_or_touch(item, dedup) {
|
||||
storage.prune_to_max(max_items);
|
||||
let _ = app.emit("clipboard-changed", ());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LAST_SEQ: std::cell::Cell<Option<u32>> = std::cell::Cell::new(None);
|
||||
}
|
||||
|
||||
fn build_text_item(t: &str) -> NewItem {
|
||||
NewItem {
|
||||
kind: "text".into(),
|
||||
content: Some(t.to_string()),
|
||||
blob: None,
|
||||
preview: make_preview(t, 200),
|
||||
size: t.len() as i64,
|
||||
hash: hash_str(t),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_image_item(dib: &[u8], w: u32, h: u32) -> NewItem {
|
||||
NewItem {
|
||||
kind: "image".into(),
|
||||
content: None,
|
||||
blob: Some(dib.to_vec()),
|
||||
preview: format!("图片 {}×{}", w, h),
|
||||
size: dib.len() as i64,
|
||||
hash: hash_bytes(dib),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_files_item(files: &[String]) -> NewItem {
|
||||
let content = serde_json::to_string(files).unwrap_or_default();
|
||||
let preview = if files.len() == 1 {
|
||||
make_preview(&files[0], 200)
|
||||
} else {
|
||||
format!(
|
||||
"{} 个文件 · {}",
|
||||
files.len(),
|
||||
make_preview(files.first().map(|s| s.as_str()).unwrap_or(""), 60)
|
||||
)
|
||||
};
|
||||
let hash = hash_str(&content);
|
||||
NewItem {
|
||||
kind: "files".into(),
|
||||
content: Some(content),
|
||||
blob: None,
|
||||
preview,
|
||||
size: files.iter().map(|f| f.len()).sum::<usize>() as i64,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_preview(s: &str, max_chars: usize) -> String {
|
||||
let single: String = s
|
||||
.chars()
|
||||
.map(|c| if c.is_control() { ' ' } else { c })
|
||||
.collect();
|
||||
let trimmed = single.trim().to_string();
|
||||
if trimmed.chars().count() <= max_chars {
|
||||
trimmed
|
||||
} else {
|
||||
let truncated: String = trimmed.chars().take(max_chars).collect();
|
||||
format!("{}…", truncated)
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_str(s: &str) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(s.as_bytes());
|
||||
format!("{:016x}", h.finish())
|
||||
}
|
||||
|
||||
fn hash_bytes(b: &[u8]) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(b);
|
||||
format!("{:016x}", h.finish())
|
||||
}
|
||||
Reference in New Issue
Block a user