348 lines
11 KiB
Rust
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(())
|
|
}
|