653 lines
21 KiB
TypeScript
653 lines
21 KiB
TypeScript
import { defineStore } from 'pinia'
|
||
import { ref } from 'vue'
|
||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||
import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
|
||
import { availableMonitors } from '@tauri-apps/api/window'
|
||
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||
import { toast } from 'vue-sonner'
|
||
import { EVENTS, STORAGE_KEYS, WINDOWS } from '@/lib/constants'
|
||
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts)
|
||
import { commands } from '@/lib/bindings'
|
||
|
||
export interface RecentCapture {
|
||
id: string
|
||
/** 缩略图 data URL(JPEG,~256px 宽,常驻内存的只有它,完整图已落盘缓存) */
|
||
thumb: string
|
||
/** 完整 PNG 缓存文件路径(按需通过 screenshot_load_cache 加载) */
|
||
filePath: string
|
||
width: number
|
||
height: number
|
||
time: number
|
||
mode: string
|
||
/** 截图时框选区域的物理屏幕坐标(贴图窗口按原位置显示;编辑器导出可能缺失) */
|
||
posX?: number
|
||
posY?: number
|
||
}
|
||
|
||
/** 截图设置(localStorage 持久化) */
|
||
export interface ScreenshotSettings {
|
||
/** 截图完成后自动保存到目录 */
|
||
autoSave: boolean
|
||
/** 自动保存目录 */
|
||
saveDir: string
|
||
/** 历史记录保留条数 */
|
||
historyLimit: number
|
||
/** 全局截图快捷键(空字符串表示禁用) */
|
||
shortcut: string
|
||
/** 贴图全局快捷键(空字符串表示禁用),再次按下关闭贴图 */
|
||
pinShortcut: string
|
||
/** 截图延时(秒),0 表示立即截图 */
|
||
delay: number
|
||
}
|
||
|
||
const SETTINGS_KEY = 'screenshot-settings'
|
||
/** 设置版本:结构变更(默认值/字段增删)时递增,清除旧数据以应用新默认值 */
|
||
const SETTINGS_VERSION = 2
|
||
const DEFAULT_SETTINGS: ScreenshotSettings = {
|
||
autoSave: false,
|
||
saveDir: '',
|
||
historyLimit: 48,
|
||
shortcut: 'ctrl+1',
|
||
pinShortcut: 'alt+1',
|
||
delay: 0,
|
||
}
|
||
|
||
export const useScreenshotStore = defineStore('screenshot', () => {
|
||
const capturing = ref(false)
|
||
const recent = ref<RecentCapture[]>([])
|
||
const settings = ref<ScreenshotSettings>({ ...DEFAULT_SETTINGS })
|
||
|
||
let exportUnlisten: UnlistenFn | null = null
|
||
let shortcutUnlisten: UnlistenFn | null = null
|
||
/** 覆盖层就绪事件监听(initOverlay 注册,destroyExportListener 一并释放) */
|
||
let overlayReadyUnlisten: UnlistenFn | null = null
|
||
/** 常驻截图覆盖层窗口(启动时创建,之后每次截图复用,避免重复 WebView 初始化) */
|
||
const OVERLAY_LABEL = WINDOWS.screenshotOverlay
|
||
let overlayWin: WebviewWindow | null = null
|
||
let overlayReadyResolve: (() => void) | null = null
|
||
let overlayReadyPromise: Promise<void> | null = null
|
||
let readyListenerInit = false
|
||
|
||
// ===== 贴图窗口 =====
|
||
const PIN_LABEL = WINDOWS.screenshotPin
|
||
const PIN_INDEX_KEY = STORAGE_KEYS.screenshotPinIndex
|
||
/** 常驻贴图窗口(首次贴图时创建,之后复用;无边框透明置顶) */
|
||
let pinWin: WebviewWindow | null = null
|
||
let pinReadyUnlisten: UnlistenFn | null = null
|
||
let pinReadyListenerInit = false
|
||
let pinReadyResolve: (() => void) | null = null
|
||
let pinReadyPromise: Promise<void> | null = null
|
||
/** 贴图全局快捷键事件监听 */
|
||
let pinShortcutUnlisten: UnlistenFn | null = null
|
||
|
||
// ===== 历史持久化 =====
|
||
const HISTORY_KEY = STORAGE_KEYS.screenshotHistory
|
||
const HISTORY_VERSION = 1
|
||
|
||
// ===== 设置持久化 =====
|
||
function loadSettings() {
|
||
try {
|
||
const raw = localStorage.getItem(SETTINGS_KEY)
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw) as Partial<ScreenshotSettings> & { version?: number }
|
||
if (parsed.version !== SETTINGS_VERSION) {
|
||
// 结构变更:重置为新默认值(应用新的默认快捷键 / 贴图快捷键 / 保留数量)
|
||
settings.value = { ...DEFAULT_SETTINGS }
|
||
saveSettings()
|
||
} else {
|
||
settings.value = { ...DEFAULT_SETTINGS, ...parsed }
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('[screenshot] 加载设置失败', e)
|
||
localStorage.removeItem(SETTINGS_KEY)
|
||
}
|
||
}
|
||
|
||
function saveSettings() {
|
||
try {
|
||
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ version: SETTINGS_VERSION, ...settings.value }))
|
||
} catch (e) {
|
||
console.error('[screenshot] 保存设置失败', e)
|
||
}
|
||
}
|
||
|
||
function setSettings(patch: Partial<ScreenshotSettings>) {
|
||
settings.value = { ...settings.value, ...patch }
|
||
saveSettings()
|
||
// 保留数量变更:立即裁剪历史(含删除超限缓存文件)
|
||
if (patch.historyLimit !== undefined) {
|
||
enforceHistoryLimit()
|
||
persistHistory()
|
||
}
|
||
}
|
||
|
||
// ===== 窗口定位 =====
|
||
/** 计算所有显示器的物理像素联合矩形(覆盖层必须用物理尺寸,保证底图 1:1 与坐标一致) */
|
||
async function computeVirtualPhysicalRect() {
|
||
const monitors = await availableMonitors()
|
||
let minX = Infinity
|
||
let minY = Infinity
|
||
let maxX = -Infinity
|
||
let maxY = -Infinity
|
||
for (const m of monitors) {
|
||
if (m.position.x < minX) minX = m.position.x
|
||
if (m.position.y < minY) minY = m.position.y
|
||
if (m.position.x + m.size.width > maxX) maxX = m.position.x + m.size.width
|
||
if (m.position.y + m.size.height > maxY) maxY = m.position.y + m.size.height
|
||
}
|
||
if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 800, height: 600 }
|
||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
||
}
|
||
|
||
// ===== 截图流程 =====
|
||
/** 启动截图:延时倒计时 → 捕获虚拟屏 → 定位常驻覆盖层 → 通知覆盖层开始 */
|
||
async function startCapture() {
|
||
if (capturing.value) return
|
||
capturing.value = true
|
||
try {
|
||
await ensureOverlay()
|
||
// 若覆盖层当前可见(上一次会话未结束),先隐藏再捕获,避免覆盖层自身出现在截图里
|
||
if (overlayWin) {
|
||
try {
|
||
if (await overlayWin.isVisible()) await overlayWin.hide()
|
||
} catch {
|
||
/* 窗口可能尚未就绪,忽略 */
|
||
}
|
||
}
|
||
// 延时倒计时(>0 时逐秒提示,避免用户错过截图时机)
|
||
const delaySec = Math.max(0, Math.floor(settings.value.delay || 0))
|
||
if (delaySec > 0) {
|
||
for (let i = delaySec; i > 0; i--) {
|
||
toast(`${i} 秒后开始截图`, { duration: 1000 })
|
||
await new Promise<void>((r) => setTimeout(r, 1000))
|
||
}
|
||
}
|
||
// 捕获虚拟屏(覆盖层隐藏 → 不会出现在截图中),仅存原始像素,不做 PNG 编码
|
||
await commands.screenshotCaptureFullscreen()
|
||
// 用物理像素把覆盖层对齐到虚拟屏(多显示器/混合 DPI 下保证底图 1:1 与坐标一致)
|
||
const rect = await computeVirtualPhysicalRect()
|
||
await overlayWin?.setPosition(new PhysicalPosition(rect.x, rect.y))
|
||
await overlayWin?.setSize(new PhysicalSize(rect.width, rect.height))
|
||
await waitOverlayReady()
|
||
await emit(EVENTS.screenshotBegin)
|
||
} catch (e) {
|
||
console.error('[screenshot] 捕获失败', e)
|
||
toast.error('截图启动失败:' + (e as Error).message)
|
||
await commands.screenshotClearFullscreen().catch(() => {})
|
||
} finally {
|
||
capturing.value = false
|
||
}
|
||
}
|
||
|
||
function resetOverlayReady() {
|
||
overlayReadyPromise = new Promise<void>((resolve) => {
|
||
overlayReadyResolve = resolve
|
||
})
|
||
}
|
||
|
||
/** 等待覆盖层就绪(首次创建时等 onMounted 完成,之后立即返回) */
|
||
async function waitOverlayReady() {
|
||
await Promise.race([
|
||
overlayReadyPromise,
|
||
new Promise<void>((r) => setTimeout(r, 3000)),
|
||
])
|
||
}
|
||
|
||
/** 创建常驻覆盖层窗口(隐藏、透明、置顶、无任务栏;启动时创建,复用直到应用退出) */
|
||
async function ensureOverlay() {
|
||
if (overlayWin) {
|
||
const existing = await WebviewWindow.getByLabel(OVERLAY_LABEL).catch(() => null)
|
||
if (existing) {
|
||
overlayWin = existing
|
||
return
|
||
}
|
||
overlayWin = null
|
||
}
|
||
resetOverlayReady()
|
||
overlayWin = new WebviewWindow(OVERLAY_LABEL, {
|
||
url: 'index.html#screenshot-overlay',
|
||
title: '截图',
|
||
x: 0,
|
||
y: 0,
|
||
width: 800,
|
||
height: 600,
|
||
// 必须 true:resizable:false 在 Windows 上会导致 setSize 失效,
|
||
// 覆盖层无法铺满虚拟屏,选区被 clamp 到初始 800x600 区域(表现为工具栏始终停在左上角)
|
||
resizable: true,
|
||
decorations: false,
|
||
alwaysOnTop: true,
|
||
skipTaskbar: true,
|
||
transparent: true,
|
||
shadow: false,
|
||
focus: false,
|
||
visible: false,
|
||
})
|
||
}
|
||
|
||
/** 初始化:注册覆盖层就绪监听并预创建常驻覆盖层窗口(应用启动时调用) */
|
||
async function initOverlay() {
|
||
if (readyListenerInit) return
|
||
readyListenerInit = true
|
||
resetOverlayReady()
|
||
overlayReadyUnlisten = await listen(EVENTS.screenshotOverlayReady, () => {
|
||
overlayReadyResolve?.()
|
||
})
|
||
await ensureOverlay()
|
||
}
|
||
|
||
async function openEditor() {
|
||
new WebviewWindow(`screenshot-editor-${Date.now()}`, {
|
||
url: 'index.html#screenshot-editor',
|
||
title: '截图编辑器',
|
||
width: 960,
|
||
height: 720,
|
||
minWidth: 640,
|
||
minHeight: 480,
|
||
decorations: false,
|
||
transparent: true,
|
||
alwaysOnTop: false,
|
||
skipTaskbar: false,
|
||
resizable: true,
|
||
shadow: true,
|
||
focus: true,
|
||
visible: true,
|
||
})
|
||
}
|
||
|
||
// ===== 历史 / 导出 =====
|
||
/** 由完整 PNG base64 生成缩略图 data URL(canvas 缩放至 ~256px 宽,JPEG 压缩) */
|
||
function makeThumb(pngBase64: string, width: number, height: number): Promise<string> {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image()
|
||
img.onload = () => {
|
||
try {
|
||
const targetW = 256
|
||
const scale = targetW / width
|
||
const targetH = Math.max(1, Math.round(height * scale))
|
||
const canvas = document.createElement('canvas')
|
||
canvas.width = targetW
|
||
canvas.height = targetH
|
||
const ctx = canvas.getContext('2d')
|
||
if (!ctx) {
|
||
reject(new Error('无法创建画布上下文'))
|
||
return
|
||
}
|
||
ctx.drawImage(img, 0, 0, targetW, targetH)
|
||
resolve(canvas.toDataURL('image/jpeg', 0.7))
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
}
|
||
img.onerror = () => reject(new Error('缩略图解码失败'))
|
||
img.src = `data:image/png;base64,${pngBase64}`
|
||
})
|
||
}
|
||
|
||
/** 按保留数量裁剪历史(超出部分删除缓存文件) */
|
||
function enforceHistoryLimit() {
|
||
const limit = Math.max(1, settings.value.historyLimit)
|
||
if (recent.value.length > limit) {
|
||
const removed = recent.value.splice(limit)
|
||
for (const item of removed) {
|
||
void commands.screenshotDeleteCache(item.filePath)
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 持久化历史元数据(含缩略图与定位坐标)到 localStorage,供重启恢复 */
|
||
function persistHistory() {
|
||
try {
|
||
localStorage.setItem(
|
||
HISTORY_KEY,
|
||
JSON.stringify({ version: HISTORY_VERSION, items: recent.value }),
|
||
)
|
||
} catch (e) {
|
||
console.error('[screenshot] 持久化历史失败(可能超出存储限额)', e)
|
||
}
|
||
}
|
||
|
||
/** 应用启动时恢复历史:版本不匹配/损坏则丢弃;并按当前保留数量清理超限文件 */
|
||
function loadHistory() {
|
||
try {
|
||
const raw = localStorage.getItem(HISTORY_KEY)
|
||
if (!raw) return
|
||
const parsed = JSON.parse(raw) as { version?: number; items?: RecentCapture[] }
|
||
if (parsed.version !== HISTORY_VERSION || !Array.isArray(parsed.items)) {
|
||
localStorage.removeItem(HISTORY_KEY)
|
||
return
|
||
}
|
||
const limit = Math.max(1, settings.value.historyLimit)
|
||
const items = parsed.items.slice(0, limit)
|
||
for (const extra of parsed.items.slice(limit)) {
|
||
void commands.screenshotDeleteCache(extra.filePath)
|
||
}
|
||
recent.value = items
|
||
} catch (e) {
|
||
console.error('[screenshot] 恢复历史失败', e)
|
||
recent.value = []
|
||
}
|
||
}
|
||
|
||
/** 追加历史项:持久化 + 删除超出上限的最旧缓存文件 */
|
||
function addRecent(
|
||
thumb: string,
|
||
filePath: string,
|
||
width: number,
|
||
height: number,
|
||
mode: string,
|
||
pos?: { x: number; y: number },
|
||
) {
|
||
recent.value.unshift({
|
||
id: crypto.randomUUID(),
|
||
thumb,
|
||
filePath,
|
||
width,
|
||
height,
|
||
time: Date.now(),
|
||
mode,
|
||
...(pos ? { posX: pos.x, posY: pos.y } : {}),
|
||
})
|
||
enforceHistoryLimit()
|
||
persistHistory()
|
||
}
|
||
|
||
/** 移除单个历史项(同时删除缓存文件) */
|
||
async function removeRecent(item: RecentCapture) {
|
||
const idx = recent.value.findIndex((r) => r.id === item.id)
|
||
if (idx >= 0) recent.value.splice(idx, 1)
|
||
persistHistory()
|
||
try {
|
||
await commands.screenshotDeleteCache(item.filePath)
|
||
} catch (e) {
|
||
console.error('[screenshot] 删除缓存失败', e)
|
||
}
|
||
}
|
||
|
||
async function clearHistory() {
|
||
for (const item of recent.value) {
|
||
void commands.screenshotDeleteCache(item.filePath)
|
||
}
|
||
recent.value = []
|
||
persistHistory()
|
||
}
|
||
|
||
/** 从历史缓存加载完整 PNG base64(一次性,不常驻) */
|
||
async function loadFullImage(item: RecentCapture): Promise<string> {
|
||
return await commands.screenshotLoadCache(item.filePath)
|
||
}
|
||
|
||
async function copyImage(pngBase64: string) {
|
||
await commands.screenshotCopyImage(pngBase64)
|
||
toast.success('已复制到剪贴板')
|
||
}
|
||
|
||
async function saveImage(pngBase64: string) {
|
||
const { save } = await import('@tauri-apps/plugin-dialog')
|
||
const ts = new Date()
|
||
.toISOString()
|
||
.replace(/[:.]/g, '-')
|
||
.slice(0, 19)
|
||
const path = await save({
|
||
defaultPath: `screenshot_${ts}.png`,
|
||
filters: [{ name: 'PNG', extensions: ['png'] }],
|
||
})
|
||
if (!path) return
|
||
await commands.screenshotSavePng(pngBase64, path)
|
||
toast.success('已保存到文件')
|
||
}
|
||
|
||
/** 覆盖层/编辑器导出处理:记录历史(缩略图 + 缓存落盘)+ 按设置自动保存 */
|
||
async function handleExport(payload: {
|
||
pngBase64: string
|
||
width: number
|
||
height: number
|
||
/** 截图时框选区域的物理屏幕坐标(贴图窗口按原位置显示;编辑器导出缺失) */
|
||
posX?: number
|
||
posY?: number
|
||
}) {
|
||
const { pngBase64, width, height, posX, posY } = payload
|
||
try {
|
||
// 完整图落盘缓存目录,历史只保留缩略图,避免完整 base64 常驻内存
|
||
const filePath = await commands.screenshotSaveCache(pngBase64)
|
||
const thumb = await makeThumb(pngBase64, width, height)
|
||
addRecent(
|
||
thumb,
|
||
filePath,
|
||
width,
|
||
height,
|
||
'capture',
|
||
posX !== undefined && posY !== undefined ? { x: posX, y: posY } : undefined,
|
||
)
|
||
} catch (e) {
|
||
console.error('[screenshot] 历史缓存失败', e)
|
||
}
|
||
// 自动保存到指定目录
|
||
if (settings.value.autoSave && settings.value.saveDir) {
|
||
const ts = new Date()
|
||
.toISOString()
|
||
.replace(/[:.]/g, '-')
|
||
.slice(0, 19)
|
||
const path = `${settings.value.saveDir.replace(/\\$/, '')}\\screenshot_${ts}.png`
|
||
try {
|
||
await commands.screenshotSavePng(pngBase64, path)
|
||
} catch (e) {
|
||
console.error('[screenshot] 自动保存失败', e)
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 监听覆盖层/编辑器导出事件(主窗口记录历史 + 自动保存) */
|
||
async function initExportListener() {
|
||
if (exportUnlisten) return
|
||
exportUnlisten = await listen<{
|
||
pngBase64: string
|
||
width: number
|
||
height: number
|
||
posX?: number
|
||
posY?: number
|
||
}>(EVENTS.screenshotExported, (e) => {
|
||
void handleExport(e.payload)
|
||
})
|
||
}
|
||
|
||
// ===== 全局快捷键 =====
|
||
/** 监听 Rust 侧 emit 的 'screenshot-shortcut' 事件(快捷键按下时触发) */
|
||
async function initShortcutListener() {
|
||
if (shortcutUnlisten) return
|
||
shortcutUnlisten = await listen(EVENTS.screenshotShortcut, () => {
|
||
void startCapture()
|
||
})
|
||
}
|
||
|
||
/** 应用启动时按已保存的快捷键注册全局热键(支持自定义,默认 Ctrl+Alt+A) */
|
||
async function initShortcutRegistration() {
|
||
try {
|
||
await commands.screenshotRegisterShortcut(settings.value.shortcut)
|
||
} catch (e) {
|
||
console.error('[screenshot] 快捷键注册失败', e)
|
||
}
|
||
}
|
||
|
||
/** 修改快捷键:持久化 + 重新注册(传空字符串禁用) */
|
||
async function setShortcut(shortcut: string) {
|
||
const next = shortcut.trim()
|
||
setSettings({ shortcut: next })
|
||
try {
|
||
await commands.screenshotRegisterShortcut(next)
|
||
return true
|
||
} catch (e) {
|
||
console.error('[screenshot] 快捷键注册失败', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// ===== 贴图窗口 =====
|
||
function resetPinReady() {
|
||
pinReadyPromise = new Promise<void>((resolve) => {
|
||
pinReadyResolve = resolve
|
||
})
|
||
}
|
||
|
||
/** 等待贴图窗口就绪(首次创建时等 onMounted 完成,之后立即返回) */
|
||
async function waitPinReady() {
|
||
await Promise.race([
|
||
pinReadyPromise,
|
||
new Promise<void>((r) => setTimeout(r, 3000)),
|
||
])
|
||
}
|
||
|
||
/** 注册贴图窗口就绪监听(首次贴图前调用) */
|
||
async function ensurePinReadyListener() {
|
||
if (pinReadyListenerInit) return
|
||
pinReadyListenerInit = true
|
||
resetPinReady()
|
||
pinReadyUnlisten = await listen(EVENTS.screenshotPinReady, () => {
|
||
pinReadyResolve?.()
|
||
})
|
||
}
|
||
|
||
/** 创建常驻贴图窗口(无边框、透明、置顶、无任务栏;首次贴图时创建,之后复用) */
|
||
async function ensurePinWindow() {
|
||
if (pinWin) {
|
||
const existing = await WebviewWindow.getByLabel(PIN_LABEL).catch(() => null)
|
||
if (existing) {
|
||
pinWin = existing
|
||
return
|
||
}
|
||
pinWin = null
|
||
}
|
||
resetPinReady()
|
||
pinWin = new WebviewWindow(PIN_LABEL, {
|
||
url: 'index.html#screenshot-pin',
|
||
title: '贴图',
|
||
x: 0,
|
||
y: 0,
|
||
width: 120,
|
||
height: 120,
|
||
// 创建时必须 resizable:true(Windows 上非 resizable 窗口 setSize 会失效);
|
||
// 每次布局结束后在贴图窗口内恢复 setResizable(false),禁用边缘拖动缩放,仅保留左键自由拖动
|
||
resizable: true,
|
||
decorations: false,
|
||
alwaysOnTop: true,
|
||
skipTaskbar: true,
|
||
transparent: true,
|
||
shadow: false,
|
||
focus: false,
|
||
visible: false,
|
||
})
|
||
await waitPinReady()
|
||
}
|
||
|
||
/**
|
||
* 切换贴图:已打开则关闭;否则把第 index 张截图贴到屏幕(原框选位置)。
|
||
* index 省略时为最近一张截图。
|
||
*/
|
||
async function togglePin(index?: number) {
|
||
if (!recent.value.length) {
|
||
toast('暂无截图可贴图')
|
||
return
|
||
}
|
||
await ensurePinReadyListener()
|
||
if (pinWin) {
|
||
try {
|
||
if (await pinWin.isVisible()) {
|
||
await pinWin.hide()
|
||
return
|
||
}
|
||
} catch {
|
||
/* 窗口可能尚未就绪,忽略 */
|
||
}
|
||
}
|
||
const i = Math.max(0, Math.min(index ?? 0, recent.value.length - 1))
|
||
localStorage.setItem(PIN_INDEX_KEY, String(i))
|
||
await ensurePinWindow()
|
||
await emit(EVENTS.screenshotPinShow)
|
||
}
|
||
|
||
/** 监听 Rust 侧 emit 的 'screenshot-pin-shortcut' 事件(贴图快捷键按下时切换) */
|
||
async function initPinShortcutListener() {
|
||
if (pinShortcutUnlisten) return
|
||
pinShortcutUnlisten = await listen(EVENTS.screenshotPinShortcut, () => {
|
||
void togglePin()
|
||
})
|
||
}
|
||
|
||
/** 应用启动时按已保存的设置注册贴图全局快捷键(默认 Alt+1) */
|
||
async function initPinShortcutRegistration() {
|
||
try {
|
||
await commands.screenshotRegisterPinShortcut(settings.value.pinShortcut)
|
||
} catch (e) {
|
||
console.error('[screenshot] 贴图快捷键注册失败', e)
|
||
}
|
||
}
|
||
|
||
/** 修改贴图快捷键:持久化 + 重新注册(传空字符串禁用) */
|
||
async function setPinShortcut(shortcut: string) {
|
||
const next = shortcut.trim()
|
||
setSettings({ pinShortcut: next })
|
||
try {
|
||
await commands.screenshotRegisterPinShortcut(next)
|
||
return true
|
||
} catch (e) {
|
||
console.error('[screenshot] 贴图快捷键注册失败', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
function destroyExportListener() {
|
||
if (exportUnlisten) {
|
||
exportUnlisten()
|
||
exportUnlisten = null
|
||
}
|
||
if (overlayReadyUnlisten) {
|
||
overlayReadyUnlisten()
|
||
overlayReadyUnlisten = null
|
||
}
|
||
if (pinReadyUnlisten) {
|
||
pinReadyUnlisten()
|
||
pinReadyUnlisten = null
|
||
}
|
||
}
|
||
|
||
function destroyShortcutListener() {
|
||
if (shortcutUnlisten) {
|
||
shortcutUnlisten()
|
||
shortcutUnlisten = null
|
||
}
|
||
if (pinShortcutUnlisten) {
|
||
pinShortcutUnlisten()
|
||
pinShortcutUnlisten = null
|
||
}
|
||
}
|
||
|
||
return {
|
||
capturing,
|
||
recent,
|
||
settings,
|
||
startCapture,
|
||
initOverlay,
|
||
openEditor,
|
||
addRecent,
|
||
clearHistory,
|
||
copyImage,
|
||
saveImage,
|
||
handleExport,
|
||
removeRecent,
|
||
loadFullImage,
|
||
loadSettings,
|
||
loadHistory,
|
||
setSettings,
|
||
setShortcut,
|
||
togglePin,
|
||
setPinShortcut,
|
||
initExportListener,
|
||
destroyExportListener,
|
||
initShortcutListener,
|
||
initShortcutRegistration,
|
||
destroyShortcutListener,
|
||
initPinShortcutListener,
|
||
initPinShortcutRegistration,
|
||
}
|
||
})
|