//! ClipboardManager:管理监听线程生命周期、设置持久化、copy_back。 use std::fs; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread::JoinHandle; use serde::{Deserialize, Serialize}; use specta::Type; use super::monitor::start_monitor; use super::reader::{write_dib, write_files, write_text}; use super::storage::Storage; /// 剪贴板设置(持久化到 clipboard/settings.json) #[derive(Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase", default)] pub struct ClipboardSettings { /// 监听是否启用 pub enabled: bool, /// 非固定历史最大条数 pub max_items: i64, /// 图片大小上限(KB),0 表示不限 pub max_image_kb: u64, pub record_text: bool, pub record_image: bool, pub record_files: bool, /// 去重(相同内容更新时间而非新增) pub dedup: bool, /// 快捷弹窗全局快捷键(如 "Alt+V",空字符串表示禁用) #[serde(default = "default_shortcut")] pub shortcut: String, } fn default_shortcut() -> String { "Alt+V".to_string() } impl Default for ClipboardSettings { fn default() -> Self { Self { enabled: true, max_items: 500, max_image_kb: 5120, record_text: true, record_image: true, record_files: true, dedup: true, shortcut: default_shortcut(), } } } pub struct ClipboardManager { storage: Arc, settings: Arc>, suppress: Arc, monitor_stop: Arc, monitor_handle: Mutex>>, settings_path: PathBuf, } impl ClipboardManager { pub fn new(app_data_dir: PathBuf) -> Self { let clip_dir = app_data_dir.join("clipboard"); let storage = match Storage::new(&clip_dir) { Ok(s) => Arc::new(s), Err(e) => { crate::logger::log_warn("clipboard", &format!("磁盘存储初始化失败,回退内存: {}", e)); Arc::new(Storage::new_in_memory()) } }; let settings_path = clip_dir.join("settings.json"); let settings = Arc::new(Mutex::new(load_settings(&settings_path))); let suppress = Arc::new(AtomicBool::new(false)); let monitor_stop = Arc::new(AtomicBool::new(true)); Self { storage, settings, suppress, monitor_stop, monitor_handle: Mutex::new(None), settings_path, } } /// 启动监听(若已运行则跳过) pub fn start(&self, app: &tauri::AppHandle) { let mut handle = self.monitor_handle.lock().unwrap_or_else(|e| e.into_inner()); if handle.is_some() { return; } self.monitor_stop.store(false, Ordering::SeqCst); let h = start_monitor( self.storage.clone(), app.clone(), self.settings.clone(), self.suppress.clone(), self.monitor_stop.clone(), ); *handle = Some(h); crate::logger::log_info("clipboard", "监听已启动"); } /// 停止监听 pub fn stop(&self) { self.monitor_stop.store(true, Ordering::SeqCst); if let Some(h) = self .monitor_handle .lock() .unwrap_or_else(|e| e.into_inner()) .take() { // 后台等待监听线程退出(最迟 800ms),不阻塞调用者。 // 剪贴板 stop 命令跑在 async runtime 线程上,直接 join 会卡住 tokio worker。 std::thread::spawn(move || { let _ = h.join(); }); } crate::logger::log_info("clipboard", "监听已停止"); } pub fn is_running(&self) -> bool { self.monitor_handle .lock() .unwrap_or_else(|e| e.into_inner()) .is_some() } pub fn get_settings(&self) -> ClipboardSettings { self.settings.lock().unwrap_or_else(|e| e.into_inner()).clone() } pub fn save_settings(&self, s: ClipboardSettings) { { *self.settings.lock().unwrap_or_else(|e| e.into_inner()) = s.clone(); } save_settings(&self.settings_path, &s); } /// 将某条历史写回剪贴板。写回前置 suppress 标志以避免再次记录。 pub fn copy_back(&self, id: i64) -> Result<(), String> { let (kind, content, blob) = self .storage .get_raw_for_copy(id) .ok_or_else(|| "条目不存在".to_string())?; self.suppress.store(true, Ordering::SeqCst); let ok = match kind.as_str() { "text" => content.as_deref().map(write_text).unwrap_or(false), "image" => blob.as_deref().map(write_dib).unwrap_or(false), "files" => { let paths: Vec = content .as_deref() .and_then(|c| serde_json::from_str(c).ok()) .unwrap_or_default(); if paths.is_empty() { false } else { write_files(&paths) } } _ => false, }; if ok { Ok(()) } else { // 写入失败也清除 suppress,避免误吞下次复制 self.suppress.store(false, Ordering::SeqCst); Err("写回剪贴板失败".into()) } } pub fn storage(&self) -> &Arc { &self.storage } } impl Drop for ClipboardManager { fn drop(&mut self) { self.stop(); } } fn load_settings(path: &Path) -> ClipboardSettings { match fs::read_to_string(path) { Ok(s) => serde_json::from_str(&s).unwrap_or_default(), Err(_) => ClipboardSettings::default(), } } fn save_settings(path: &Path, s: &ClipboardSettings) { if let Some(parent) = path.parent() { let _ = fs::create_dir_all(parent); } if let Ok(json) = serde_json::to_string_pretty(s) { let _ = fs::write(path, json); } }