Files
Thing/src-tauri/src/clipboard/manager.rs
T
2026-08-06 10:33:16 +08:00

200 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<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) => {
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<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);
}
}