This commit is contained in:
zhongluofeng
2026-07-30 09:09:29 +08:00
parent 74902c4cec
commit f452322aad
32 changed files with 4586 additions and 115 deletions
+187
View File
@@ -0,0 +1,187 @@
//! 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 super::monitor::start_monitor;
use super::reader::{write_dib, write_files, write_text};
use super::storage::Storage;
/// 剪贴板设置(持久化到 clipboard/settings.json
#[derive(Clone, Serialize, Deserialize)]
#[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<Storage>,
settings: Arc<Mutex<ClipboardSettings>>,
suppress: Arc<AtomicBool>,
monitor_stop: Arc<AtomicBool>,
monitor_handle: Mutex<Option<JoinHandle<()>>>,
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) => {
eprintln!("[clipboard] 磁盘存储初始化失败,回退内存: {}", 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();
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);
eprintln!("[clipboard] 监听已启动");
}
/// 停止监听
pub fn stop(&self) {
self.monitor_stop.store(true, Ordering::SeqCst);
if let Some(h) = self.monitor_handle.lock().unwrap().take() {
// 不阻塞等待;轮询线程最迟 800ms 后退出
let _ = h.join();
}
eprintln!("[clipboard] 监听已停止");
}
pub fn is_running(&self) -> bool {
self.monitor_handle.lock().unwrap().is_some()
}
pub fn get_settings(&self) -> ClipboardSettings {
self.settings.lock().unwrap().clone()
}
pub fn save_settings(&self, s: ClipboardSettings) {
{
*self.settings.lock().unwrap() = 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<String> = 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<Storage> {
&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);
}
}