635 lines
21 KiB
Rust
635 lines
21 KiB
Rust
//! Tauri 命令:截图模块
|
||
//!
|
||
//! 命令清单:
|
||
//! - screenshot_capture_fullscreen:捕获虚拟屏并存入静态(不做 PNG 编码),返回捕获时刻光标坐标
|
||
//! - screenshot_get_fullscreen_bmp:取出全屏捕获的 BMP 原始字节(raw IPC,覆盖层显示用,不移除)
|
||
//! - screenshot_fullscreen_png:全屏捕获编码 PNG base64 并清除(全屏截图进编辑器用)
|
||
//! - screenshot_clear_fullscreen:清除静态全屏捕获(覆盖层关闭时)
|
||
//! - screenshot_crop_stored:按物理像素裁剪已存储的全屏捕获
|
||
//! - screenshot_pick_list:枚举可拾取顶层窗口(Z 序,前端缓存后本地命中测试)
|
||
//! - screenshot_show_overlay:一次 IPC 完成覆盖层 show + focus(关键路径减少往返)
|
||
//! - screenshot_enum_windows:枚举可见顶层窗口
|
||
//! - screenshot_capture_window:按 hwnd 捕获指定窗口
|
||
//! - screenshot_scroll_capture / screenshot_scroll_start / screenshot_scroll_finish /
|
||
//! screenshot_scroll_cancel:滚动截图(同步一次调用 / 会话式:启动、完成、取消)
|
||
//! - screenshot_set_scroll_hole:滚动模式遮罩挖孔(防 Chromium 遮挡检测冻结目标窗口)
|
||
//! - screenshot_take_editor_image_raw:取出编辑器图片(raw IPC,滚动截图会话直接写入)
|
||
//! - screenshot_compose_png / screenshot_compose_copy:raw RGBA → PNG(仅编码 / 剪贴板+编码)
|
||
//! - screenshot_copy_image:写入剪贴板(CF_DIB)
|
||
//! - screenshot_save_png:写入文件
|
||
//! - screenshot_disable_transitions:禁用窗口显示/隐藏过渡动画(消除覆盖层缩放动画)
|
||
|
||
use super::{CaptureData, CaptureStart, WindowInfo};
|
||
use tauri::{AppHandle, Emitter, Manager};
|
||
|
||
/// 禁用指定窗口(按 label 查找)的显示/隐藏过渡动画,消除覆盖层出现/消失时的缩放动画
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_disable_transitions(
|
||
app: tauri::AppHandle,
|
||
label: String,
|
||
) -> Result<(), String> {
|
||
#[cfg(windows)]
|
||
{
|
||
use raw_window_handle::HasWindowHandle;
|
||
let win = app
|
||
.get_webview_window(&label)
|
||
.ok_or_else(|| format!("窗口不存在: {}", label))?;
|
||
let handle = win
|
||
.window_handle()
|
||
.map_err(|e| format!("获取窗口句柄失败: {}", e))?;
|
||
match handle.as_raw() {
|
||
raw_window_handle::RawWindowHandle::Win32(h) => {
|
||
super::capture::disable_window_transitions(h.hwnd.get() as isize)
|
||
}
|
||
_ => Ok(()),
|
||
}
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = (app, label);
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// 注册(或切换)截图全局快捷键。传入空字符串则禁用快捷键。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_register_shortcut(
|
||
app: tauri::AppHandle,
|
||
shortcut: String,
|
||
) -> Result<(), String> {
|
||
crate::shortcut::register_shortcut(&app, "截图", &shortcut, |a| {
|
||
let _ = a.emit(crate::constants::events::SCREENSHOT_SHORTCUT, ());
|
||
})
|
||
}
|
||
|
||
/// 注销截图全局快捷键
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_unregister_shortcut(app: tauri::AppHandle) -> Result<(), String> {
|
||
crate::shortcut::unregister_shortcut(&app, "截图");
|
||
Ok(())
|
||
}
|
||
|
||
/// 注册(或切换)贴图全局快捷键。传入空字符串则禁用快捷键。
|
||
/// 按下时 emit 'screenshot-pin-shortcut',由前端切换贴图窗口显示/隐藏。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_register_pin_shortcut(
|
||
app: tauri::AppHandle,
|
||
shortcut: String,
|
||
) -> Result<(), String> {
|
||
crate::shortcut::register_shortcut(&app, "贴图", &shortcut, |a| {
|
||
let _ = a.emit(crate::constants::events::SCREENSHOT_PIN_SHORTCUT, ());
|
||
})
|
||
}
|
||
|
||
/// 注销贴图全局快捷键
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_unregister_pin_shortcut(app: tauri::AppHandle) -> Result<(), String> {
|
||
crate::shortcut::unregister_shortcut(&app, "贴图");
|
||
Ok(())
|
||
}
|
||
|
||
/// 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码。
|
||
/// 同时返回捕获时刻的光标物理坐标(覆盖层据此做初始窗口拾取,省一次 IPC 往返)。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_capture_fullscreen() -> Result<CaptureStart, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
// 屏幕捕获涉及 GDI 调用,放线程池避免阻塞 async 调度
|
||
tauri::async_runtime::spawn_blocking(|| {
|
||
let img = super::capture::capture_virtual_screen()?;
|
||
super::capture::store_fullscreen(img)?;
|
||
let (cursor_x, cursor_y) = super::capture::cursor_pos().unwrap_or((0, 0));
|
||
Ok(CaptureStart { cursor_x, cursor_y })
|
||
})
|
||
.await
|
||
.map_err(|e| format!("捕获任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 取出全屏捕获的 BMP 原始字节(raw IPC → 前端 ArrayBuffer),不移除
|
||
#[tauri::command]
|
||
pub async fn screenshot_get_fullscreen_bmp() -> Result<tauri::ipc::Response, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(|| {
|
||
let bmp = super::capture::fullscreen_bmp()?;
|
||
Ok(tauri::ipc::Response::new(bmp))
|
||
})
|
||
.await
|
||
.map_err(|e| format!("读取捕获失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 全屏捕获编码为 PNG base64 并清除(全屏截图直接进编辑器)
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_fullscreen_png() -> Result<CaptureData, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(|| super::capture::fullscreen_png())
|
||
.await
|
||
.map_err(|e| format!("编码任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 清除静态全屏捕获(覆盖层关闭/取消时释放内存)
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_clear_fullscreen() -> Result<(), String> {
|
||
#[cfg(windows)]
|
||
{
|
||
super::capture::clear_fullscreen();
|
||
Ok(())
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// 按物理像素坐标裁剪已存储的全屏捕获
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_crop_stored(
|
||
x: i32,
|
||
y: i32,
|
||
w: i32,
|
||
h: i32,
|
||
) -> Result<CaptureData, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
super::capture::crop_stored(x, y, w, h)
|
||
})
|
||
.await
|
||
.map_err(|e| format!("裁剪任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = (x, y, w, h);
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 裁剪已存储的全屏捕获并直接写入剪贴板(一次 IPC 完成"裁剪+复制")
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_crop_copy_stored(
|
||
x: i32,
|
||
y: i32,
|
||
w: i32,
|
||
h: i32,
|
||
) -> Result<CaptureData, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
super::capture::crop_copy_stored(x, y, w, h)
|
||
})
|
||
.await
|
||
.map_err(|e| format!("裁剪复制任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = (x, y, w, h);
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 枚举可拾取的顶层窗口(Z 序顶→底,排除本进程/不可见/工具窗口)。
|
||
/// 前端在截图开始时缓存列表,鼠标移动时在 JS 侧本地命中测试,消除逐帧 IPC 往返。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_pick_list() -> Result<Vec<WindowInfo>, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(super::capture::pick_windows)
|
||
.await
|
||
.map_err(|e| format!("枚举失败: {}", e))
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Ok(vec![])
|
||
}
|
||
}
|
||
|
||
/// 一次 IPC 完成指定窗口的显示与聚焦(截图覆盖层显示关键路径,减少串行往返)
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_show_overlay(
|
||
app: tauri::AppHandle,
|
||
label: String,
|
||
) -> Result<(), String> {
|
||
let win = app
|
||
.get_webview_window(&label)
|
||
.ok_or_else(|| format!("窗口不存在: {}", label))?;
|
||
win.show().map_err(|e| format!("显示窗口失败: {}", e))?;
|
||
win.set_focus().map_err(|e| format!("聚焦窗口失败: {}", e))?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 获取当前鼠标物理屏幕坐标(贴图窗口拖动跟随等场景使用)
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_cursor_pos() -> Result<(i32, i32), String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(super::capture::cursor_pos)
|
||
.await
|
||
.map_err(|e| format!("查询任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 枚举所有可见顶层窗口
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_enum_windows() -> Result<Vec<WindowInfo>, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(|| super::capture::enum_visible_windows())
|
||
.await
|
||
.map_err(|e| format!("枚举失败: {}", e))
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Ok(vec![])
|
||
}
|
||
}
|
||
|
||
/// 按 hwnd 捕获指定窗口
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_capture_window(hwnd: isize) -> Result<CaptureData, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
let img = super::capture::capture_window(hwnd)?;
|
||
Ok(super::CaptureData {
|
||
png_base64: super::capture::base64_encode(&img.png),
|
||
width: img.width,
|
||
height: img.height,
|
||
})
|
||
})
|
||
.await
|
||
.map_err(|e| format!("捕获任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = hwnd;
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 滚动截图:从窗口当前滚动位置向下拼接到底部,返回超长 PNG。
|
||
/// `region` 为 Some 时仅在框选区域(屏幕物理坐标)内捕捉,宽 = 选区宽;
|
||
/// 为 None 时捕捉整个客户区。结束后会把窗口滚回起始位置,不打扰用户。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_scroll_capture(
|
||
hwnd: isize,
|
||
region: Option<super::ScrollRegion>,
|
||
) -> Result<CaptureData, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
super::scroll_capture::scroll_capture(hwnd, region)
|
||
})
|
||
.await
|
||
.map_err(|e| format!("滚动截图任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = hwnd;
|
||
let _ = region;
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 启动滚动截图会话(后台线程持续捕捉拼接,实时推进度事件)。
|
||
/// `auto = true` 为自动滚动(线程主动下滚拼到底部);`false` 为手动(等用户滚动窗口)。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub fn screenshot_scroll_start(
|
||
app: AppHandle,
|
||
hwnd: isize,
|
||
region: Option<super::ScrollRegion>,
|
||
auto: bool,
|
||
) -> Result<(), String> {
|
||
#[cfg(windows)]
|
||
{
|
||
super::scroll_session::start(app, hwnd, region, auto)
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = (app, hwnd, region, auto);
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 结束滚动截图会话并导出结果。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub fn screenshot_scroll_finish() -> Result<(), String> {
|
||
#[cfg(windows)]
|
||
{
|
||
super::scroll_session::finish()
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 取消滚动截图会话(不导出)。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub fn screenshot_scroll_cancel() -> Result<(), String> {
|
||
#[cfg(windows)]
|
||
{
|
||
super::scroll_session::cancel_now()
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 滚动模式遮罩挖孔:在截图覆盖层窗口上挖出选区带的真孔(region = None 时复位整窗)。
|
||
///
|
||
/// Chromium 系浏览器(Edge/Chrome)的窗口遮挡检测会把被完全覆盖的窗口标记为
|
||
/// occluded 并暂停渲染——滚动截图时覆盖层铺满全屏,网页"看起来完全不滚动"。
|
||
/// 挖孔后目标窗口仅部分被覆盖,恢复渲染与滚轮响应(详见 capture::set_scroll_hole)。
|
||
/// 进入滚动模式时带选区调用,会话结束/新一轮截图开始时必须传 None 复位。
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub fn screenshot_set_scroll_hole(
|
||
app: AppHandle,
|
||
region: Option<super::ScrollRegion>,
|
||
) -> Result<(), String> {
|
||
#[cfg(windows)]
|
||
{
|
||
use raw_window_handle::HasWindowHandle;
|
||
|
||
let mut hwnds: Vec<isize> = Vec::new();
|
||
for (label, win) in app.webview_windows() {
|
||
if label.starts_with(crate::constants::windows::SCREENSHOT_OVERLAY) {
|
||
let hwnd = win
|
||
.window_handle()
|
||
.ok()
|
||
.and_then(|h| match h.as_raw() {
|
||
raw_window_handle::RawWindowHandle::Win32(w) => {
|
||
Some(w.hwnd.get() as isize)
|
||
}
|
||
_ => None,
|
||
});
|
||
if let Some(h) = hwnd {
|
||
hwnds.push(h);
|
||
}
|
||
}
|
||
}
|
||
if hwnds.is_empty() {
|
||
return Err("截图覆盖层窗口不存在".into());
|
||
}
|
||
for h in hwnds {
|
||
super::capture::set_scroll_hole(h, region)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = (app, region);
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// 取出编辑器图片(原始 PNG 字节,raw IPC → 前端 ArrayBuffer → Blob URL,取出即清除)
|
||
///
|
||
/// 长图(滚动截图)可达数十 MB:raw IPC 相比 base64 JSON 事件传输省 ~33% 体积,
|
||
/// 且避免 JSON 序列化/多次广播。注:返回 ipc::Response,specta 无法生成,豁免标注。
|
||
#[tauri::command]
|
||
pub async fn screenshot_take_editor_image_raw() -> Result<tauri::ipc::Response, String> {
|
||
match super::take_editor_image_raw() {
|
||
Some(bytes) => Ok(tauri::ipc::Response::new(bytes)),
|
||
None => Err("无待编辑的截图".into()),
|
||
}
|
||
}
|
||
|
||
/// 将 PNG base64 写入系统剪贴板(转 CF_DIB)
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_copy_image(png_base64: String) -> Result<(), String> {
|
||
#[cfg(windows)]
|
||
{
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
super::capture::copy_png_to_clipboard(&png_base64)
|
||
})
|
||
.await
|
||
.map_err(|e| format!("剪贴板任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = png_base64;
|
||
Err("剪贴板仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 有标注导出:接收 raw RGBA(前端 canvas.getImageData 直传),一次完成 剪贴板+PNG base64。
|
||
///
|
||
/// 省去前端 toDataURL(PNG 编码+base64) → Rust base64 解码 → PNG 解码 三次往返。
|
||
/// body 格式:前 8 字节 = width(i32 LE) + height(i32 LE),之后为 raw RGBA 像素。
|
||
/// 注:参数为 tauri::ipc::Request(原始 body),specta 无法生成,豁免标注。
|
||
#[tauri::command]
|
||
pub async fn screenshot_compose_copy(
|
||
request: tauri::ipc::Request<'_>,
|
||
) -> Result<super::CaptureData, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
let body = match request.body() {
|
||
tauri::ipc::InvokeBody::Raw(data) => data.clone(),
|
||
_ => return Err("需要 raw body(ArrayBuffer)".into()),
|
||
};
|
||
if body.len() < 8 {
|
||
return Err("数据不足:缺少尺寸头".into());
|
||
}
|
||
let width = i32::from_le_bytes([body[0], body[1], body[2], body[3]]);
|
||
let height = i32::from_le_bytes([body[4], body[5], body[6], body[7]]);
|
||
let rgba = body[8..].to_vec();
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
super::capture::compose_copy_rgba(&rgba, width, height)
|
||
})
|
||
.await
|
||
.map_err(|e| format!("合成复制任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = request;
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 有标注导出(仅编码):接收 raw RGBA(前端 canvas.getImageData 直传),一次完成 PNG 编码。
|
||
/// 与 screenshot_compose_copy 的区别:不写剪贴板(编辑器「保存到文件」用)。
|
||
/// body 格式:前 8 字节 = width(i32 LE) + height(i32 LE),之后为 raw RGBA 像素。
|
||
/// 注:参数为 tauri::ipc::Request(原始 body),specta 无法生成,豁免标注。
|
||
#[tauri::command]
|
||
pub async fn screenshot_compose_png(
|
||
request: tauri::ipc::Request<'_>,
|
||
) -> Result<super::CaptureData, String> {
|
||
#[cfg(windows)]
|
||
{
|
||
let body = match request.body() {
|
||
tauri::ipc::InvokeBody::Raw(data) => data.clone(),
|
||
_ => return Err("需要 raw body(ArrayBuffer)".into()),
|
||
};
|
||
if body.len() < 8 {
|
||
return Err("数据不足:缺少尺寸头".into());
|
||
}
|
||
let width = i32::from_le_bytes([body[0], body[1], body[2], body[3]]);
|
||
let height = i32::from_le_bytes([body[4], body[5], body[6], body[7]]);
|
||
let rgba = body[8..].to_vec();
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
super::capture::compose_png_rgba(&rgba, width, height)
|
||
})
|
||
.await
|
||
.map_err(|e| format!("合成编码任务失败: {}", e))?
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = request;
|
||
Err("截图仅支持 Windows".into())
|
||
}
|
||
}
|
||
|
||
/// 将 PNG base64 写入文件
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_save_png(png_base64: String, path: String) -> Result<(), String> {
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
#[cfg(windows)]
|
||
{
|
||
super::capture::save_png_to_file(&png_base64, &path)
|
||
}
|
||
#[cfg(not(windows))]
|
||
{
|
||
let _ = (png_base64, path);
|
||
Err("文件写入仅支持 Windows".to_string())
|
||
}
|
||
})
|
||
.await
|
||
.map_err(|e| format!("保存任务失败: {}", e))?
|
||
}
|
||
|
||
// ===== 截图历史缓存:完整 PNG 落盘到应用数据目录(持久化,随历史保留数量清理),内存只保留缩略图 =====
|
||
|
||
/// 历史根目录(app_data_dir/screenshot/history)
|
||
fn history_cache_dir(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
|
||
let dir = app
|
||
.path()
|
||
.app_data_dir()
|
||
.map_err(|e| format!("获取应用数据目录失败: {}", e))?
|
||
.join("screenshot")
|
||
.join("history");
|
||
std::fs::create_dir_all(&dir).map_err(|e| format!("创建历史目录失败: {}", e))?;
|
||
Ok(dir)
|
||
}
|
||
|
||
/// 校验 path 属于历史缓存目录(防止路径穿越/任意文件读写)
|
||
fn ensure_in_history_dir(app: &tauri::AppHandle, path: &str) -> Result<std::path::PathBuf, String> {
|
||
let dir = history_cache_dir(app)?;
|
||
let p = std::path::PathBuf::from(path);
|
||
if !p.starts_with(&dir) {
|
||
return Err("非法路径:不在截图历史缓存目录内".into());
|
||
}
|
||
Ok(p)
|
||
}
|
||
|
||
/// 将完整 PNG 写入历史缓存目录,返回文件路径
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_save_cache(
|
||
app: tauri::AppHandle,
|
||
png_base64: String,
|
||
) -> Result<String, String> {
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
let dir = history_cache_dir(&app)?;
|
||
// 时间戳微秒命名(避免引入额外依赖;并发截图的同微秒碰撞可忽略)
|
||
let ts = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.map(|d| d.as_micros())
|
||
.unwrap_or(0);
|
||
let path = dir.join(format!("{}.png", ts));
|
||
super::capture::save_png_to_file(&png_base64, &path.to_string_lossy())?;
|
||
Ok(path.to_string_lossy().into_owned())
|
||
})
|
||
.await
|
||
.map_err(|e| format!("缓存任务失败: {}", e))?
|
||
}
|
||
|
||
/// 从历史缓存目录读取 PNG 并返回 base64(点击历史项复制/保存时一次性加载,不常驻内存)
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_load_cache(
|
||
app: tauri::AppHandle,
|
||
path: String,
|
||
) -> Result<String, String> {
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
let p = ensure_in_history_dir(&app, &path)?;
|
||
let bytes = std::fs::read(&p).map_err(|e| format!("读取缓存失败: {}", e))?;
|
||
use base64::Engine as _;
|
||
Ok(base64::engine::general_purpose::STANDARD.encode(bytes))
|
||
})
|
||
.await
|
||
.map_err(|e| format!("读取缓存任务失败: {}", e))?
|
||
}
|
||
|
||
/// 读取历史缓存 PNG 原始字节(raw IPC → 前端 ArrayBuffer,贴图窗口显示用:
|
||
/// 跳过 base64 编码,IPC 传输与前端内存占用均省 ~33%;同 get_fullscreen_bmp 豁免 specta)
|
||
#[tauri::command]
|
||
pub async fn screenshot_load_cache_raw(
|
||
app: tauri::AppHandle,
|
||
path: String,
|
||
) -> Result<tauri::ipc::Response, String> {
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
let p = ensure_in_history_dir(&app, &path)?;
|
||
let bytes = std::fs::read(&p).map_err(|e| format!("读取缓存失败: {}", e))?;
|
||
Ok(tauri::ipc::Response::new(bytes))
|
||
})
|
||
.await
|
||
.map_err(|e| format!("读取缓存任务失败: {}", e))?
|
||
}
|
||
|
||
/// 删除历史缓存文件(历史项移除/清空时调用,静默忽略不存在文件)
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn screenshot_delete_cache(app: tauri::AppHandle, path: String) -> Result<(), String> {
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
if let Ok(p) = ensure_in_history_dir(&app, &path) {
|
||
let _ = std::fs::remove_file(p);
|
||
}
|
||
Ok(())
|
||
})
|
||
.await
|
||
.map_err(|e| format!("删除缓存任务失败: {}", e))?
|
||
}
|