200 lines
6.1 KiB
Rust
200 lines
6.1 KiB
Rust
//! 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);
|
||
}
|
||
}
|