截图模块初始化
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { availableMonitors } from '@tauri-apps/api/window'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
interface CaptureData {
|
||||
pngBase64: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface RecentCapture {
|
||||
id: string
|
||||
pngBase64: string
|
||||
width: number
|
||||
height: number
|
||||
time: number
|
||||
mode: string
|
||||
}
|
||||
|
||||
export type CaptureMode = 'region' | 'window' | 'fullscreen'
|
||||
|
||||
const OVERLAY_LABEL = 'screenshot-overlay'
|
||||
const EDITOR_LABEL = 'screenshot-editor'
|
||||
|
||||
export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
const capturing = ref(false)
|
||||
const recent = ref<RecentCapture[]>([])
|
||||
let exportUnlisten: UnlistenFn | null = null
|
||||
|
||||
/** 计算所有显示器的逻辑像素联合矩形(用于覆盖层窗口定位/尺寸) */
|
||||
async function computeVirtualLogicalRect() {
|
||||
const monitors = await availableMonitors()
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
for (const m of monitors) {
|
||||
const s = m.scaleFactor || 1
|
||||
const lx = m.position.x / s
|
||||
const ly = m.position.y / s
|
||||
const lw = m.size.width / s
|
||||
const lh = m.size.height / s
|
||||
if (lx < minX) minX = lx
|
||||
if (ly < minY) minY = ly
|
||||
if (lx + lw > maxX) maxX = lx + lw
|
||||
if (ly + lh > maxY) maxY = ly + lh
|
||||
}
|
||||
if (!Number.isFinite(minX)) {
|
||||
minX = 0
|
||||
minY = 0
|
||||
maxX = 800
|
||||
maxY = 600
|
||||
}
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
||||
}
|
||||
|
||||
/** 启动截图:region=区域选择,window=窗口拾取,fullscreen=直接进编辑器 */
|
||||
async function startCapture(mode: CaptureMode) {
|
||||
if (capturing.value) return
|
||||
capturing.value = true
|
||||
try {
|
||||
// 1. 捕获虚拟屏(覆盖层尚未创建 → 不会出现在截图中)
|
||||
const data = await invoke<CaptureData>('screenshot_capture_fullscreen')
|
||||
|
||||
if (mode === 'fullscreen') {
|
||||
// 全屏截图直接送入编辑器
|
||||
await invoke('screenshot_set_editor_image', { pngBase64: data.pngBase64 })
|
||||
// 清掉静态全屏缓存(编辑器用 cropped/全图,不再需要原始 BGRA)
|
||||
await invoke('screenshot_take_fullscreen').catch(() => {})
|
||||
await openEditor()
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 区域/窗口模式:创建覆盖层选区窗口
|
||||
await openOverlay(mode)
|
||||
} catch (e) {
|
||||
console.error('[screenshot] 捕获失败', e)
|
||||
toast.error('截图启动失败:' + (e as Error).message)
|
||||
// 失败时清理静态缓存
|
||||
await invoke('screenshot_take_fullscreen').catch(() => {})
|
||||
} finally {
|
||||
capturing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openOverlay(mode: CaptureMode) {
|
||||
const existing = await WebviewWindow.getByLabel(OVERLAY_LABEL)
|
||||
if (existing) await existing.close()
|
||||
|
||||
const rect = await computeVirtualLogicalRect()
|
||||
const url = `index.html#screenshot-overlay?mode=${mode}`
|
||||
new WebviewWindow(OVERLAY_LABEL, {
|
||||
url,
|
||||
title: '截图',
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
decorations: false,
|
||||
transparent: true,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
shadow: false,
|
||||
focus: true,
|
||||
visible: true,
|
||||
})
|
||||
}
|
||||
|
||||
async function openEditor() {
|
||||
const existing = await WebviewWindow.getByLabel(EDITOR_LABEL)
|
||||
if (existing) {
|
||||
await existing.show()
|
||||
await existing.setFocus()
|
||||
return
|
||||
}
|
||||
new WebviewWindow(EDITOR_LABEL, {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
function addRecent(pngBase64: string, width: number, height: number, mode: string) {
|
||||
recent.value.unshift({
|
||||
id: crypto.randomUUID(),
|
||||
pngBase64,
|
||||
width,
|
||||
height,
|
||||
time: Date.now(),
|
||||
mode,
|
||||
})
|
||||
if (recent.value.length > 12) recent.value.pop()
|
||||
}
|
||||
|
||||
async function copyImage(pngBase64: string) {
|
||||
await invoke('screenshot_copy_image', { 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 invoke('screenshot_save_png', { pngBase64, path })
|
||||
toast.success('已保存到文件')
|
||||
}
|
||||
|
||||
/** 监听编辑器导出事件(主窗口记录历史 + 提示) */
|
||||
async function initExportListener() {
|
||||
if (exportUnlisten) return
|
||||
exportUnlisten = await listen<{
|
||||
pngBase64: string
|
||||
width: number
|
||||
height: number
|
||||
}>('screenshot-exported', (e) => {
|
||||
addRecent(e.payload.pngBase64, e.payload.width, e.payload.height, 'edited')
|
||||
})
|
||||
}
|
||||
|
||||
function destroyExportListener() {
|
||||
if (exportUnlisten) {
|
||||
exportUnlisten()
|
||||
exportUnlisten = null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
capturing,
|
||||
recent,
|
||||
startCapture,
|
||||
openEditor,
|
||||
addRecent,
|
||||
copyImage,
|
||||
saveImage,
|
||||
initExportListener,
|
||||
destroyExportListener,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user