滚动截图,细节调整
This commit is contained in:
@@ -311,6 +311,8 @@ export const useMonitorStore = defineStore('monitor', () => {
|
||||
const snapshot = ref<SensorSnapshot | null>(null)
|
||||
const kernelInfo = ref<MonitorKernelInfo | null>(null)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
/** 提权运行但 PawnIO 驱动缺失(CPU 温度/功耗大概率无法读取) */
|
||||
const pawnIoMissing = ref(false)
|
||||
/** 累计收到的 monitor-data 事件数,用于诊断与"已连接"判定 */
|
||||
const eventCount = ref(0)
|
||||
/** 最近一次收到 monitor-data 的时间戳(ms) */
|
||||
@@ -690,10 +692,12 @@ export const useMonitorStore = defineStore('monitor', () => {
|
||||
// OSD 开启时同步推送(合并到每帧一次,避免与网速事件重复推送)
|
||||
if (osdConfig.value.overlayEnabled) scheduleOsdPush()
|
||||
}))
|
||||
unlistenFns.push(await listen(EVENTS.monitorReady, () => {
|
||||
unlistenFns.push(await listen<{ isAdmin?: boolean; pawnIoInstalled?: boolean }>(EVENTS.monitorReady, (e) => {
|
||||
refreshStatus()
|
||||
// Kernel 就绪后主动拉取一次快照,避免等待 SSE 首事件导致 UI 空白
|
||||
fetchSnapshot()
|
||||
// PawnIO 驱动缺失检测:提权运行但仍缺驱动 → CPU 温度/功耗大概率无法读取
|
||||
pawnIoMissing.value = e.payload?.isAdmin === true && e.payload?.pawnIoInstalled === false
|
||||
}))
|
||||
unlistenFns.push(await listen(EVENTS.monitorLoading, () => {
|
||||
// 后端正在等待 Kernel ready,刷新状态以反映 running=true
|
||||
@@ -1154,6 +1158,22 @@ export const useMonitorStore = defineStore('monitor', () => {
|
||||
void pushOsdState()
|
||||
})
|
||||
osdEventUnlisteners.push(unlistenReq)
|
||||
// 监听游戏全屏事件:前台出现全屏应用(游戏)时隐藏 OSD,
|
||||
// 避免透明置顶悬浮窗占用 DWM 合成路径导致游戏掉帧;退出全屏后恢复。
|
||||
const unlistenGameActive = await listen(EVENTS.osdGameActive, async () => {
|
||||
gameFullscreen.value = true
|
||||
if (osdConfig.value.overlayEnabled && osdConfig.value.gameAutoHide) {
|
||||
await hideOverlayWindow()
|
||||
}
|
||||
})
|
||||
osdEventUnlisteners.push(unlistenGameActive)
|
||||
const unlistenGameInactive = await listen(EVENTS.osdGameInactive, async () => {
|
||||
gameFullscreen.value = false
|
||||
if (osdConfig.value.overlayEnabled && osdConfig.value.gameAutoHide) {
|
||||
await ensureOverlayWindow()
|
||||
}
|
||||
})
|
||||
osdEventUnlisteners.push(unlistenGameInactive)
|
||||
}
|
||||
|
||||
/** initOsd 幂等守卫:监听/配置 watcher 只注册一次(App 启动 + 模块挂载均会调用) */
|
||||
@@ -1277,6 +1297,7 @@ export const useMonitorStore = defineStore('monitor', () => {
|
||||
snapshot,
|
||||
kernelInfo,
|
||||
errorMsg,
|
||||
pawnIoMissing,
|
||||
eventCount,
|
||||
lastEventTime,
|
||||
starting,
|
||||
|
||||
@@ -253,8 +253,48 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
await ensureOverlay()
|
||||
}
|
||||
|
||||
async function openEditor() {
|
||||
new WebviewWindow(`screenshot-editor-${Date.now()}`, {
|
||||
// ===== 常驻截图编辑器窗口 =====
|
||||
const EDITOR_LABEL = WINDOWS.screenshotEditor
|
||||
let editorWin: WebviewWindow | null = null
|
||||
let editorReadyUnlisten: UnlistenFn | null = null
|
||||
let editorReadyListenerInit = false
|
||||
let editorReadyResolve: (() => void) | null = null
|
||||
let editorReadyPromise: Promise<void> | null = null
|
||||
|
||||
function resetEditorReady() {
|
||||
editorReadyPromise = new Promise<void>((resolve) => {
|
||||
editorReadyResolve = resolve
|
||||
})
|
||||
}
|
||||
|
||||
async function waitEditorReady() {
|
||||
await Promise.race([
|
||||
editorReadyPromise,
|
||||
new Promise<void>((r) => setTimeout(r, 5000)),
|
||||
])
|
||||
}
|
||||
|
||||
async function ensureEditorListener() {
|
||||
if (editorReadyListenerInit) return
|
||||
editorReadyListenerInit = true
|
||||
resetEditorReady()
|
||||
editorReadyUnlisten = await listen(EVENTS.screenshotEditorReady, () => {
|
||||
editorReadyResolve?.()
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建常驻编辑器窗口(隐藏,首次打开时创建一次,之后 show 复用不重建 WebView) */
|
||||
async function ensureEditorWindow() {
|
||||
if (editorWin) {
|
||||
const existing = await WebviewWindow.getByLabel(EDITOR_LABEL).catch(() => null)
|
||||
if (existing) {
|
||||
editorWin = existing
|
||||
return
|
||||
}
|
||||
editorWin = null
|
||||
}
|
||||
resetEditorReady()
|
||||
editorWin = new WebviewWindow(EDITOR_LABEL, {
|
||||
url: 'index.html#screenshot-editor',
|
||||
title: '截图编辑器',
|
||||
width: 960,
|
||||
@@ -268,10 +308,18 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
resizable: true,
|
||||
shadow: true,
|
||||
focus: true,
|
||||
visible: true,
|
||||
visible: false,
|
||||
})
|
||||
}
|
||||
|
||||
/** 打开编辑器:确保常驻窗口就绪后通知其加载编辑器图片槽中的新图(图由 Rust 侧直接写入) */
|
||||
async function openEditor() {
|
||||
await ensureEditorListener()
|
||||
await ensureEditorWindow()
|
||||
await waitEditorReady()
|
||||
await emit(EVENTS.screenshotEditorLoad)
|
||||
}
|
||||
|
||||
// ===== 历史 / 导出 =====
|
||||
/** 由完整 PNG base64 生成缩略图 data URL(canvas 缩放至 ~256px 宽,JPEG 压缩) */
|
||||
function makeThumb(pngBase64: string, width: number, height: number): Promise<string> {
|
||||
@@ -468,6 +516,17 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 滚动截图完成 → 进编辑器 =====
|
||||
/** 滚动截图完成的监听(覆盖层通知 → 打开常驻编辑器;PNG 原始字节已由 Rust 写入编辑器图片槽) */
|
||||
let scrollEditorUnlisten: UnlistenFn | null = null
|
||||
|
||||
async function initScrollEditorListener() {
|
||||
if (scrollEditorUnlisten) return
|
||||
scrollEditorUnlisten = await listen(EVENTS.scrollToEditor, () => {
|
||||
void openEditor()
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 全局快捷键 =====
|
||||
/** 监听 Rust 侧 emit 的 'screenshot-shortcut' 事件(快捷键按下时触发) */
|
||||
async function initShortcutListener() {
|
||||
@@ -627,6 +686,14 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
pinReadyUnlisten()
|
||||
pinReadyUnlisten = null
|
||||
}
|
||||
if (editorReadyUnlisten) {
|
||||
editorReadyUnlisten()
|
||||
editorReadyUnlisten = null
|
||||
}
|
||||
if (scrollEditorUnlisten) {
|
||||
scrollEditorUnlisten()
|
||||
scrollEditorUnlisten = null
|
||||
}
|
||||
}
|
||||
|
||||
function destroyShortcutListener() {
|
||||
@@ -662,6 +729,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
||||
setPinShortcut,
|
||||
initExportListener,
|
||||
destroyExportListener,
|
||||
initScrollEditorListener,
|
||||
initShortcutListener,
|
||||
initShortcutRegistration,
|
||||
destroyShortcutListener,
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { EVENTS } from '@/lib/constants'
|
||||
import { useMonitorStore } from '@/stores/monitorStore'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
|
||||
const logger = createLogger('updater')
|
||||
|
||||
/** 更新进度事件载荷(与 Rust UpdateProgress 对应) */
|
||||
export interface UpdateProgress {
|
||||
stage: string
|
||||
percent: number
|
||||
downloadedBytes: number
|
||||
totalBytes: number | null
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用 / ThingHK 内核更新的全局编排状态。
|
||||
*
|
||||
* 把更新进度与"need_stop 确认"从集团件作用域提升到全局 store:
|
||||
* 切换模块导致 GeneralSettings 卸载后,进行中的下载进度、need_stop 等待、
|
||||
* done 后的内核重启都能跨组件存活并正确完成,避免"切回来进度丢失 / 更新卡死 / 不重启内核"。
|
||||
*/
|
||||
export const useUpdaterStore = defineStore('updater', () => {
|
||||
/** 应用本体更新中 */
|
||||
const appUpdating = ref(false)
|
||||
/** ThingHK 内核更新中 */
|
||||
const kernelUpdating = ref(false)
|
||||
/** 更新进度(下载/解压/need_stop/done 共用一条进度展示) */
|
||||
const progress = ref<UpdateProgress | null>(null)
|
||||
/** 应用更新下载中可取消(下载完成进入 applying 后不可取消) */
|
||||
const downloadCancellable = ref(false)
|
||||
/** ThingHK 内核包下载中可取消 */
|
||||
const kernelDownloadCancellable = ref(false)
|
||||
/** 停止监控内核的确认弹窗状态(need_stop 阶段由后端 event 触发) */
|
||||
const thinghkConfirmState = ref<{
|
||||
open: boolean
|
||||
wasRunning: boolean
|
||||
busy: boolean
|
||||
resolved: boolean
|
||||
}>({
|
||||
open: false,
|
||||
wasRunning: false,
|
||||
busy: false,
|
||||
resolved: false,
|
||||
})
|
||||
|
||||
let subscribed = false
|
||||
const unlisteners: UnlistenFn[] = []
|
||||
let handlingNeedStop = false
|
||||
|
||||
/** 若应用被最小化/隐藏到后台,先弹到前台再显示确认框 */
|
||||
const focusMain = () => {
|
||||
invoke('quickpanel_focus_main_window').catch(() => { /* 忽略 */ })
|
||||
}
|
||||
|
||||
/** need_stop:记录内核是否在运行,弹出确认框等待用户停止并继续 */
|
||||
function handleNeedStop() {
|
||||
if (handlingNeedStop) return
|
||||
handlingNeedStop = true
|
||||
try {
|
||||
focusMain()
|
||||
const monitor = useMonitorStore()
|
||||
const wasRunning = monitor.status?.running ?? false
|
||||
thinghkConfirmState.value = { open: true, wasRunning, busy: false, resolved: false }
|
||||
} finally {
|
||||
handlingNeedStop = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 全局订阅 UPDATE_PROGRESS:need_stop / done / error 与组件挂载无关,切换标签不中断更新 */
|
||||
async function subscribe() {
|
||||
if (subscribed) return
|
||||
subscribed = true
|
||||
try {
|
||||
unlisteners.push(
|
||||
await listen<UpdateProgress>(EVENTS.updateProgress, (e) => {
|
||||
progress.value = e.payload
|
||||
const p = e.payload
|
||||
if (p.stage === 'need_stop') {
|
||||
handleNeedStop()
|
||||
} else if (p.stage === 'done') {
|
||||
const wasRunning = thinghkConfirmState.value.wasRunning
|
||||
kernelUpdating.value = false
|
||||
progress.value = null
|
||||
// 更新前内核在运行 → 替换完成后恢复(重启内核),保持内核状态
|
||||
if (wasRunning) {
|
||||
const monitor = useMonitorStore()
|
||||
void monitor.start()
|
||||
}
|
||||
} else if (p.stage === 'error') {
|
||||
kernelUpdating.value = false
|
||||
progress.value = null
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error('[updater] 订阅更新进度失败: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 确认停止内核并唤醒后端 apply 继续解压替换 */
|
||||
async function confirmNeedStop() {
|
||||
const s = thinghkConfirmState.value
|
||||
if (s.resolved) return
|
||||
s.resolved = true
|
||||
s.busy = true
|
||||
try {
|
||||
if (s.wasRunning) {
|
||||
const monitor = useMonitorStore()
|
||||
await monitor.stop()
|
||||
}
|
||||
await invoke('update_thinghk_confirm')
|
||||
} catch (e) {
|
||||
logger.error('[updater] 停止监控内核失败: ' + e)
|
||||
// 停止失败则中止更新,避免替换阶段因 exe 占用而报错
|
||||
try {
|
||||
await invoke('update_thinghk_cancel')
|
||||
} catch { /* 忽略 */ }
|
||||
} finally {
|
||||
s.busy = false
|
||||
s.open = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消 ThingHK 内核更新(中止后端等待中的 apply 流程) */
|
||||
function cancelNeedStop() {
|
||||
const s = thinghkConfirmState.value
|
||||
if (s.resolved) return
|
||||
s.resolved = true
|
||||
s.open = false
|
||||
invoke('update_thinghk_cancel').catch(() => { /* 忽略 */ })
|
||||
}
|
||||
|
||||
/** 复位全局更新状态(订阅与状态一并清理,供测试/异常恢复用) */
|
||||
function reset() {
|
||||
appUpdating.value = false
|
||||
kernelUpdating.value = false
|
||||
progress.value = null
|
||||
downloadCancellable.value = false
|
||||
kernelDownloadCancellable.value = false
|
||||
thinghkConfirmState.value = { open: false, wasRunning: false, busy: false, resolved: false }
|
||||
subscribed = false
|
||||
unlisteners.forEach((fn) => fn())
|
||||
unlisteners.length = 0
|
||||
}
|
||||
|
||||
return {
|
||||
appUpdating,
|
||||
kernelUpdating,
|
||||
progress,
|
||||
downloadCancellable,
|
||||
kernelDownloadCancellable,
|
||||
thinghkConfirmState,
|
||||
subscribe,
|
||||
confirmNeedStop,
|
||||
cancelNeedStop,
|
||||
reset,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user