托盘
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user