Files
Thing/src-tauri/src/clipboard/commands.rs
T

348 lines
11 KiB
Rust

//! 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<String> {
dib_to_png(b).map(|png| STANDARD.encode(&png))
}
/// 历史查询结果(含总数,用于分页)
#[derive(Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct HistoryPage {
pub items: Vec<ClipboardItem>,
pub total: i64,
}
#[tauri::command]
#[specta::specta]
pub async fn clipboard_get_history(
limit: Option<i64>,
offset: Option<i64>,
kind: Option<String>,
manager: State<'_, ClipboardManager>,
) -> Result<HistoryPage, String> {
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<Vec<ClipboardItem>, 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<i64>,
offset: Option<i64>,
manager: State<'_, ClipboardManager>,
) -> Result<HistoryPage, String> {
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<Option<ClipboardItemDetail>, 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<Option<String>, 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<bool, String> {
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<bool, String> {
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<bool, String> {
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<i64, String> {
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<ClipboardSettings, String> {
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<ClipboardStatus, String> {
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(())
}