//! Tauri 命令:剪贴板模块 use base64::engine::general_purpose::STANDARD; use base64::Engine as _; use serde::Serialize; use specta::Type; use tauri::{AppHandle, State}; use super::manager::{ClipboardManager, ClipboardSettings}; use super::reader::{dib_to_png, dib_to_thumbnail}; use super::storage::{ClipboardItem, ClipboardItemDetail}; #[derive(Serialize, Type)] #[serde(rename_all = "camelCase")] pub struct ClipboardStatus { pub running: bool, pub count: i64, } fn img_to_base64(b: &[u8]) -> Option { dib_to_png(b).map(|png| STANDARD.encode(&png)) } /// 历史查询结果(含总数,用于分页) #[derive(Serialize, Type)] #[serde(rename_all = "camelCase")] pub struct HistoryPage { pub items: Vec, pub total: i64, } #[tauri::command] #[specta::specta] pub async fn clipboard_get_history( limit: Option, offset: Option, kind: Option, manager: State<'_, ClipboardManager>, ) -> Result { let storage = manager.storage().clone(); let kind = kind.unwrap_or_else(|| "all".into()); let limit = limit.unwrap_or(50); let offset = offset.unwrap_or(0); // SQLite 查询移出 async runtime 线程 tauri::async_runtime::spawn_blocking(move || { let items = storage.get_history(limit, offset, &kind); let total = storage.count_kind(&kind); HistoryPage { items, total } }) .await .map_err(|e| format!("查询任务失败: {}", e)) } #[tauri::command] #[specta::specta] pub async fn clipboard_get_pinned( manager: State<'_, ClipboardManager>, ) -> Result, String> { let storage = manager.storage().clone(); tauri::async_runtime::spawn_blocking(move || storage.get_pinned()) .await .map_err(|e| format!("查询任务失败: {}", e)) } #[tauri::command] #[specta::specta] pub async fn clipboard_search( query: String, limit: Option, offset: Option, manager: State<'_, ClipboardManager>, ) -> Result { let storage = manager.storage().clone(); let limit = limit.unwrap_or(50); let offset = offset.unwrap_or(0); tauri::async_runtime::spawn_blocking(move || { let items = storage.search(&query, limit, offset); let total = storage.count_search(&query); HistoryPage { items, total } }) .await .map_err(|e| format!("查询任务失败: {}", e)) } #[tauri::command] #[specta::specta] pub async fn clipboard_get_item( id: i64, manager: State<'_, ClipboardManager>, ) -> Result, String> { let storage = manager.storage().clone(); // get_detail 内部:锁内取数 + 锁外 base64 编码,整体移出主线程 tauri::async_runtime::spawn_blocking(move || storage.get_detail(id, img_to_base64)) .await .map_err(|e| format!("查询任务失败: {}", e)) } /// 获取图片缩略图 PNG base64(弹窗悬停预览用,避免加载全尺寸图片) #[tauri::command] #[specta::specta] pub async fn clipboard_get_thumb( id: i64, manager: State<'_, ClipboardManager>, ) -> Result, String> { let storage = manager.storage().clone(); tauri::async_runtime::spawn_blocking(move || { storage.get_thumb(id, |dib| dib_to_thumbnail(dib, 256)) }) .await .map_err(|e| format!("查询任务失败: {}", e)) } #[tauri::command] #[specta::specta] pub async fn clipboard_set_pinned( id: i64, pinned: bool, manager: State<'_, ClipboardManager>, ) -> Result { let storage = manager.storage().clone(); tauri::async_runtime::spawn_blocking(move || storage.set_pinned(id, pinned)) .await .map_err(|e| format!("操作任务失败: {}", e)) } #[tauri::command] #[specta::specta] pub async fn clipboard_delete( id: i64, manager: State<'_, ClipboardManager>, ) -> Result { let storage = manager.storage().clone(); tauri::async_runtime::spawn_blocking(move || storage.delete(id)) .await .map_err(|e| format!("删除任务失败: {}", e)) } #[tauri::command] #[specta::specta] pub async fn clipboard_clear(manager: State<'_, ClipboardManager>) -> Result { let storage = manager.storage().clone(); tauri::async_runtime::spawn_blocking(move || storage.clear_non_pinned()) .await .map_err(|e| format!("清空任务失败: {}", e)) } #[tauri::command] #[specta::specta] pub async fn clipboard_copy_back( id: i64, manager: State<'_, ClipboardManager>, ) -> Result<(), String> { manager.copy_back(id) } #[tauri::command] #[specta::specta] pub async fn clipboard_count(manager: State<'_, ClipboardManager>) -> Result { let storage = manager.storage().clone(); tauri::async_runtime::spawn_blocking(move || storage.count()) .await .map_err(|e| format!("查询任务失败: {}", e)) } #[tauri::command] #[specta::specta] pub async fn clipboard_get_settings( manager: State<'_, ClipboardManager>, ) -> Result { Ok(manager.get_settings()) } #[tauri::command] #[specta::specta] pub async fn clipboard_save_settings( settings: ClipboardSettings, app: AppHandle, manager: State<'_, ClipboardManager>, ) -> Result<(), String> { let prev = manager.get_settings(); let prev_enabled = prev.enabled; let prev_shortcut = prev.shortcut.clone(); // 快捷键变化且新值非空:先注册(原子化 + 冲突检测),成功后才保存设置。 // 注册失败时恢复旧快捷键并中止保存,避免设置被写入无法生效的组合键、旧键丢失。 if settings.shortcut != prev_shortcut && !settings.shortcut.trim().is_empty() { if let Err(e) = crate::shortcut::register_shortcut(&app, "剪贴板", &settings.shortcut, |a| { super::popup::show_popup(a) }) { // register_shortcut 内部已注销旧快捷键,失败时需重新注册旧键以恢复 if !prev_shortcut.trim().is_empty() { let _ = crate::shortcut::register_shortcut(&app, "剪贴板", &prev_shortcut, |a| { super::popup::show_popup(a) }); } return Err(e); } // 新快捷键非空时确保弹窗/预览窗口已预创建 super::popup::ensure_popup_window(&app); super::popup::ensure_preview_window(&app); } // 保存设置 manager.save_settings(settings.clone()); // 监听开关变化时联动启停 if settings.enabled && !prev_enabled { manager.start(&app); } else if !settings.enabled && prev_enabled { manager.stop(); } // 快捷键改为空字符串(禁用):注销旧快捷键 if settings.shortcut.trim().is_empty() && settings.shortcut != prev_shortcut { crate::shortcut::unregister_shortcut(&app, "剪贴板"); } Ok(()) } #[tauri::command] #[specta::specta] pub async fn clipboard_status( manager: State<'_, ClipboardManager>, ) -> Result { let storage = manager.storage().clone(); let running = manager.is_running(); let count = tauri::async_runtime::spawn_blocking(move || storage.count()) .await .map_err(|e| format!("查询任务失败: {}", e))?; Ok(ClipboardStatus { running, count }) } #[tauri::command] #[specta::specta] pub async fn clipboard_start(app: AppHandle, manager: State<'_, ClipboardManager>) -> Result<(), String> { manager.start(&app); Ok(()) } #[tauri::command] #[specta::specta] pub async fn clipboard_stop(manager: State<'_, ClipboardManager>) -> Result<(), String> { manager.stop(); Ok(()) } /// 注册(或切换)快捷弹窗全局快捷键 #[tauri::command] #[specta::specta] pub async fn clipboard_register_shortcut( shortcut: String, app: AppHandle, ) -> Result<(), String> { crate::shortcut::register_shortcut(&app, "剪贴板", &shortcut, |a| { super::popup::show_popup(a) }) } /// 注销快捷弹窗全局快捷键 #[tauri::command] #[specta::specta] pub async fn clipboard_unregister_shortcut(app: AppHandle) -> Result<(), String> { crate::shortcut::unregister_shortcut(&app, "剪贴板"); Ok(()) } /// 手动触发显示快捷弹窗(供 UI 按钮调用) #[tauri::command] #[specta::specta] pub async fn clipboard_show_popup(app: AppHandle) -> Result<(), String> { super::popup::show_popup(&app); Ok(()) } /// 隐藏快捷弹窗 #[tauri::command] #[specta::specta] pub async fn clipboard_hide_popup(app: AppHandle) -> Result<(), String> { super::popup::hide_popup(&app); Ok(()) } /// 显示已创建的弹窗窗口(前端 onMounted 后调用) #[tauri::command] #[specta::specta] pub async fn clipboard_show_window(app: AppHandle) -> Result<(), String> { super::popup::show_window(&app); Ok(()) } /// 隐藏弹窗并模拟 Ctrl+V 粘贴到原窗口 #[tauri::command] #[specta::specta] pub async fn clipboard_paste_to_target(app: AppHandle) -> Result<(), String> { super::popup::paste_to_target(&app); Ok(()) } /// 在弹窗旁显示独立预览窗口(悬停/键盘选中时调用) #[tauri::command] #[specta::specta] pub async fn clipboard_show_preview(app: AppHandle, id: i64) -> Result<(), String> { super::popup::show_preview(&app, id); Ok(()) } /// 隐藏独立预览窗口 #[tauri::command] #[specta::specta] pub async fn clipboard_hide_preview(app: AppHandle) -> Result<(), String> { super::popup::hide_preview(&app); Ok(()) } /// 按内容自适应调整预览窗大小(逻辑像素)。前端加载内容(文本测高、图片按宽高比)后调用, /// 窗口贴合内容消除留白;后端按弹窗所在屏工作区钳制并重新对齐弹窗。 /// allow_flip:初始落位为 true(优先侧放不下可换侧);放大/还原为 false(保持原侧)。 #[tauri::command] #[specta::specta] pub async fn clipboard_resize_preview( app: AppHandle, width: f64, height: f64, allow_flip: bool, ) -> Result<(), String> { super::popup::resize_preview(&app, width, height, allow_flip); Ok(()) } /// 显示已就绪的预览窗口。前端完成内容加载与 resize 后调用,窗口以最终尺寸出现, /// 消除"先以上次尺寸(可能是放大态大窗)显示再缩回"的闪烁。 #[tauri::command] #[specta::specta] pub async fn clipboard_reveal_preview(app: AppHandle) -> Result<(), String> { super::popup::reveal_preview(&app); Ok(()) } /// 预览窗交互锁定:前端预览窗收到 mousedown(放大/缩小、复制、选择文本)时调用。 /// 此后弹窗+预览不因失焦/鼠标离开而关闭,仅当点击外部或弹窗重新聚焦时退出锁定。 #[tauri::command] #[specta::specta] pub async fn clipboard_preview_interacted(app: AppHandle) -> Result<(), String> { super::popup::preview_interacted(&app); Ok(()) }