优化,贴图
This commit is contained in:
+255
-14
@@ -5,7 +5,7 @@ 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, WINDOWS } from '@/lib/constants'
|
||||
import { EVENTS, STORAGE_KEYS, WINDOWS } from '@/lib/constants'
|
||||
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts)
|
||||
import { commands } from '@/lib/bindings'
|
||||
|
||||
@@ -19,6 +19,9 @@ export interface RecentCapture {
|
||||
height: number
|
||||
time: number
|
||||
mode: string
|
||||
/** 截图时框选区域的物理屏幕坐标(贴图窗口按原位置显示;编辑器导出可能缺失) */
|
||||
posX?: number
|
||||
posY?: number
|
||||
}
|
||||
|
||||
/** 截图设置(localStorage 持久化) */
|
||||
@@ -31,16 +34,21 @@ export interface ScreenshotSettings {
|
||||
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: 12,
|
||||
shortcut: 'ctrl+alt+a',
|
||||
historyLimit: 48,
|
||||
shortcut: 'ctrl+1',
|
||||
pinShortcut: 'alt+1',
|
||||
delay: 0,
|
||||
}
|
||||
|
||||
@@ -51,6 +59,8 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
|
||||
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
|
||||
@@ -58,13 +68,35 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
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>
|
||||
settings.value = { ...DEFAULT_SETTINGS, ...parsed }
|
||||
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)
|
||||
@@ -74,7 +106,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
|
||||
function saveSettings() {
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings.value))
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ version: SETTINGS_VERSION, ...settings.value }))
|
||||
} catch (e) {
|
||||
console.error('[screenshot] 保存设置失败', e)
|
||||
}
|
||||
@@ -83,6 +115,11 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
function setSettings(patch: Partial<ScreenshotSettings>) {
|
||||
settings.value = { ...settings.value, ...patch }
|
||||
saveSettings()
|
||||
// 保留数量变更:立即裁剪历史(含删除超限缓存文件)
|
||||
if (patch.historyLimit !== undefined) {
|
||||
enforceHistoryLimit()
|
||||
persistHistory()
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 窗口定位 =====
|
||||
@@ -193,7 +230,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
if (readyListenerInit) return
|
||||
readyListenerInit = true
|
||||
resetOverlayReady()
|
||||
await listen(EVENTS.screenshotOverlayReady, () => {
|
||||
overlayReadyUnlisten = await listen(EVENTS.screenshotOverlayReady, () => {
|
||||
overlayReadyResolve?.()
|
||||
})
|
||||
await ensureOverlay()
|
||||
@@ -247,9 +284,8 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 追加历史项:删除超出上限的最旧缓存文件 */
|
||||
function addRecent(thumb: string, filePath: string, width: number, height: number, mode: string) {
|
||||
recent.value.unshift({ id: crypto.randomUUID(), thumb, filePath, width, height, time: Date.now(), mode })
|
||||
/** 按保留数量裁剪历史(超出部分删除缓存文件) */
|
||||
function enforceHistoryLimit() {
|
||||
const limit = Math.max(1, settings.value.historyLimit)
|
||||
if (recent.value.length > limit) {
|
||||
const removed = recent.value.splice(limit)
|
||||
@@ -259,10 +295,68 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 持久化历史元数据(含缩略图与定位坐标)到 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) {
|
||||
@@ -275,6 +369,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
void commands.screenshotDeleteCache(item.filePath)
|
||||
}
|
||||
recent.value = []
|
||||
persistHistory()
|
||||
}
|
||||
|
||||
/** 从历史缓存加载完整 PNG base64(一次性,不常驻) */
|
||||
@@ -303,13 +398,27 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
}
|
||||
|
||||
/** 覆盖层/编辑器导出处理:记录历史(缩略图 + 缓存落盘)+ 按设置自动保存 */
|
||||
async function handleExport(payload: { pngBase64: string; width: number; height: number }) {
|
||||
const { pngBase64, width, height } = payload
|
||||
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')
|
||||
addRecent(
|
||||
thumb,
|
||||
filePath,
|
||||
width,
|
||||
height,
|
||||
'capture',
|
||||
posX !== undefined && posY !== undefined ? { x: posX, y: posY } : undefined,
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('[screenshot] 历史缓存失败', e)
|
||||
}
|
||||
@@ -335,7 +444,9 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
pngBase64: string
|
||||
width: number
|
||||
height: number
|
||||
}>('screenshot-exported', (e) => {
|
||||
posX?: number
|
||||
posY?: number
|
||||
}>(EVENTS.screenshotExported, (e) => {
|
||||
void handleExport(e.payload)
|
||||
})
|
||||
}
|
||||
@@ -371,11 +482,132 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 贴图窗口 =====
|
||||
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() {
|
||||
@@ -383,6 +615,10 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
shortcutUnlisten()
|
||||
shortcutUnlisten = null
|
||||
}
|
||||
if (pinShortcutUnlisten) {
|
||||
pinShortcutUnlisten()
|
||||
pinShortcutUnlisten = null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -400,12 +636,17 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
removeRecent,
|
||||
loadFullImage,
|
||||
loadSettings,
|
||||
loadHistory,
|
||||
setSettings,
|
||||
setShortcut,
|
||||
togglePin,
|
||||
setPinShortcut,
|
||||
initExportListener,
|
||||
destroyExportListener,
|
||||
initShortcutListener,
|
||||
initShortcutRegistration,
|
||||
destroyShortcutListener,
|
||||
initPinShortcutListener,
|
||||
initPinShortcutRegistration,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user