更新饥荒
This commit is contained in:
@@ -70,6 +70,46 @@ updater.init({
|
||||
setQuitting: (v) => { isQuitting = v },
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 屏幕模式:window 普通窗口 / windowFull 窗口全屏(最大化铺满工作区+隐藏菜单栏)/
|
||||
// full 真全屏(覆盖任务栏)。渲染进程经 preload 的 setScreenMode 切换;
|
||||
// 菜单「视图 → 全屏」(F11) 与 Esc 退全屏通过 enter/leave-full-screen 事件回同步。
|
||||
// 模式持久化到 config.json,下次启动恢复。
|
||||
// ---------------------------------------------------------------------
|
||||
let screenMode = 'window'
|
||||
let prevWindowed = 'window' // 从真全屏退出时回到的窗口模式
|
||||
|
||||
function applyWindowedMode(win, mode) {
|
||||
if (!win || win.isDestroyed()) return
|
||||
if (mode === 'windowFull') {
|
||||
if (!win.isMaximized()) win.maximize()
|
||||
win.setAutoHideMenuBar(true)
|
||||
win.setMenuBarVisibility(false)
|
||||
} else {
|
||||
if (win.isMaximized()) win.unmaximize()
|
||||
win.setAutoHideMenuBar(false)
|
||||
win.setMenuBarVisibility(true)
|
||||
}
|
||||
}
|
||||
|
||||
function setScreenModeInMain(mode) {
|
||||
if (!mainWin || mainWin.isDestroyed()) return { mode: screenMode }
|
||||
if (mode === 'full') {
|
||||
if (!mainWin.isFullScreen()) mainWin.setFullScreen(true)
|
||||
screenMode = 'full'
|
||||
} else {
|
||||
if (mainWin.isFullScreen()) mainWin.setFullScreen(false)
|
||||
prevWindowed = mode === 'windowFull' ? 'windowFull' : 'window'
|
||||
screenMode = prevWindowed
|
||||
applyWindowedMode(mainWin, prevWindowed)
|
||||
}
|
||||
writeCfg({ screenMode })
|
||||
mainWin.webContents.send('nlg:screen-mode-changed', screenMode)
|
||||
return { mode: screenMode }
|
||||
}
|
||||
|
||||
ipcMain.handle('nlg:set-screen-mode', (_e, mode) => setScreenModeInMain(mode === 'full' ? 'full' : mode === 'windowFull' ? 'windowFull' : 'window'))
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 服务器设置窗口(极简内嵌页面,深色风格与主站一致)
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -214,7 +254,7 @@ function buildMenu() {
|
||||
{ label: '缩小', role: 'zoomOut' },
|
||||
{ label: '实际大小', role: 'resetZoom' },
|
||||
{ type: 'separator' },
|
||||
{ label: '全屏', role: 'togglefullscreen' },
|
||||
{ label: '全屏', role: 'togglefullscreen', accelerator: 'F11' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -264,15 +304,35 @@ function createWindow() {
|
||||
},
|
||||
})
|
||||
mainWin.once('ready-to-show', () => {
|
||||
if (cfg.winMax) mainWin.maximize()
|
||||
// 恢复上次屏幕模式:full=真全屏;windowFull=窗口全屏(最大化+隐藏菜单栏)
|
||||
if (cfg.screenMode === 'full') {
|
||||
screenMode = 'full'
|
||||
mainWin.setFullScreen(true)
|
||||
} else {
|
||||
screenMode = prevWindowed = cfg.screenMode === 'windowFull' ? 'windowFull' : 'window'
|
||||
applyWindowedMode(mainWin, screenMode)
|
||||
if (screenMode === 'window' && cfg.winMax) mainWin.maximize() // 普通窗口保留「最大化启动」记忆
|
||||
}
|
||||
mainWin.show()
|
||||
// 启动后 3 秒静默检查一次更新(已是最新版不弹窗,发现新版本才提示)
|
||||
setTimeout(() => { updater.checkUpdate(true).catch(() => {}) }, 3000)
|
||||
})
|
||||
// 原生全屏状态变化回同步(菜单 F11 / Esc 退出全屏时渲染层才能拿到正确状态)
|
||||
mainWin.on('enter-full-screen', () => {
|
||||
screenMode = 'full'
|
||||
writeCfg({ screenMode })
|
||||
if (!mainWin.isDestroyed()) mainWin.webContents.send('nlg:screen-mode-changed', screenMode)
|
||||
})
|
||||
mainWin.on('leave-full-screen', () => {
|
||||
screenMode = prevWindowed
|
||||
applyWindowedMode(mainWin, prevWindowed)
|
||||
writeCfg({ screenMode })
|
||||
if (!mainWin.isDestroyed()) mainWin.webContents.send('nlg:screen-mode-changed', screenMode)
|
||||
})
|
||||
// 点关闭:记住窗口状态并隐藏到托盘(冒烟模式 / 真正退出时直接关)
|
||||
mainWin.on('close', (e) => {
|
||||
try {
|
||||
writeCfg({ winBounds: mainWin.getNormalBounds(), winMax: mainWin.isMaximized() })
|
||||
writeCfg({ winBounds: mainWin.getNormalBounds(), winMax: mainWin.isMaximized(), screenMode })
|
||||
} catch {}
|
||||
if (!isQuitting && !SMOKE && tray) {
|
||||
e.preventDefault()
|
||||
|
||||
136
electron/mario-feel-check.cjs
Normal file
136
electron/mario-feel-check.cjs
Normal file
@@ -0,0 +1,136 @@
|
||||
// 手感实测(开发用临时脚本):进入 1-1 后用 __marioProbe 采样,
|
||||
// 测站立满跳/助跑跳的高度与滞空、连跳节奏、按住不自动回弹、走/跑极速
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const path = require('path')
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
async function main() {
|
||||
const win = new BrowserWindow({ width: 1280, height: 860, show: true, backgroundColor: '#0f1220', webPreferences: { backgroundThrottling: false } })
|
||||
win.webContents.setBackgroundThrottling(false)
|
||||
const pageErrors = []
|
||||
win.webContents.on('console-message', (e) => { if (e?.level === 'error' && e?.message) pageErrors.push(e.message) })
|
||||
const js = (code) => win.webContents.executeJavaScript(code, true).catch((e) => { console.error('[feel] JS 失败:', String(e.message).slice(0, 80)); return null })
|
||||
const waitFor = async (cond, t, d) => {
|
||||
const t0 = Date.now()
|
||||
while (Date.now() - t0 < t) { try { if (await js(cond)) return } catch {} ; await sleep(300) }
|
||||
throw new Error('超时:' + d)
|
||||
}
|
||||
const key = async (k, down) => js(`window.dispatchEvent(new KeyboardEvent('${down ? 'keydown' : 'keyup'}', { key: '${k}' })); true`)
|
||||
// 以 ~28ms 间隔采样探针
|
||||
const sample = async (ms) => {
|
||||
const out = []
|
||||
const t0 = Date.now()
|
||||
while (Date.now() - t0 < ms) {
|
||||
const p = await js(`JSON.stringify(window.__marioProbe || null)`)
|
||||
if (p) out.push({ ...JSON.parse(p), t: Date.now() - t0 })
|
||||
await sleep(25)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
await win.loadURL('http://localhost:5173')
|
||||
await js('localStorage.clear(); true')
|
||||
await win.webContents.reload()
|
||||
await waitFor(`!!document.querySelector('input[placeholder^="用户名"]')`, 10000, '登录页')
|
||||
await js(`
|
||||
(() => {
|
||||
const us = document.querySelector('input[placeholder^="用户名"]')
|
||||
us.value = 'mariotest01'; us.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
const ps = document.querySelector('input[type="password"]')
|
||||
ps.value = 'test123456'; ps.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
document.querySelector('button.submit').click()
|
||||
return true
|
||||
})()
|
||||
`)
|
||||
await waitFor(`!!localStorage.getItem('token')`, 8000, '登录')
|
||||
await sleep(1200)
|
||||
await js(`location.href = '/play/mario'; true`).catch(() => {})
|
||||
await waitFor(`!!document.querySelector('.overlay button') && [...document.querySelectorAll('.overlay button')].some((b) => /开始闯关|继续第|开始游戏/.test(b.textContent))`, 20000, '游玩页')
|
||||
await js(`
|
||||
(() => {
|
||||
const b = document.querySelector('.overlay button.btn-lg') ||
|
||||
[...document.querySelectorAll('.overlay button')].find((x) => /开始闯关|继续第|开始游戏/.test(x.textContent))
|
||||
b.click()
|
||||
return true
|
||||
})()
|
||||
`)
|
||||
await sleep(900) // 选关画面出现,光标在最新解锁关(12 → 11=3-4)
|
||||
await key('ArrowRight', true); await sleep(50); await key('ArrowRight', false); await sleep(80) // → 0 = 1-1
|
||||
await key(' ', true); await sleep(80); await key(' ', false)
|
||||
await sleep(2600) // 等横幅结束
|
||||
|
||||
// —— 测 A:站立满跳(按住)——
|
||||
const groundY0 = (await js(`window.__marioProbe.y + window.__marioProbe.h ?? null`)) // 无 h,用落点近似
|
||||
const a = []
|
||||
await key(' ', true)
|
||||
const aS = await sample(1300)
|
||||
await key(' ', false)
|
||||
await sleep(600)
|
||||
// —— 测 C:连跳(原地按 ~0.83s 滞空节奏快速重按,验证落地前缓冲让连跳无缝)——
|
||||
await key('x', true)
|
||||
await sleep(300)
|
||||
const cS = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await key(' ', true)
|
||||
const s1 = await sample(280)
|
||||
await key(' ', false)
|
||||
await sleep(430) // 重按落在落地前缓冲窗内
|
||||
cS.push(...s1)
|
||||
}
|
||||
await key(' ', false)
|
||||
await key('x', false)
|
||||
await sleep(500)
|
||||
// —— 测 B:走/跑极速(地面 0-18 列约 608px,控制跑动时长别掉坑)——
|
||||
await key('ArrowRight', true)
|
||||
const walkS = await sample(900) // 未按住跑步键 = 走
|
||||
await key('x', true)
|
||||
await sleep(300)
|
||||
const runS = await sample(900) // 按住 x = 跑
|
||||
await key('x', false)
|
||||
await key('ArrowRight', false)
|
||||
await sleep(400)
|
||||
// —— 测 D:按住跳不放 ——
|
||||
await sleep(500)
|
||||
await key(' ', true)
|
||||
const dS = await sample(1600)
|
||||
await key(' ', false)
|
||||
|
||||
const analyze = (s, label) => {
|
||||
if (!s.length) return null
|
||||
const gY = Math.max(...s.map((p) => p.y)) // 最大 y 即地面站立位
|
||||
const air = s.filter((p) => !p.ground)
|
||||
const apex = air.length ? gY - Math.min(...air.map((p) => p.y)) : 0
|
||||
// 起落对:ground false 的连续段
|
||||
let hops = 0
|
||||
let prev = true
|
||||
let airMs = 0
|
||||
const dts = []
|
||||
for (let i = 1; i < s.length; i++) dts.push(s[i].t - s[i - 1].t)
|
||||
for (const p of s) {
|
||||
if (prev && !p.ground) hops++
|
||||
if (!p.ground) airMs += 28
|
||||
prev = p.ground
|
||||
}
|
||||
return { label, apexPx: apex.toFixed(0), hops, airMsPerHop: hops ? Math.round(airMs / hops) : 0, groundY: gY.toFixed(0) }
|
||||
}
|
||||
// 极速:相邻采样位移/时间
|
||||
const topSpeed = (s) => {
|
||||
let best = 0
|
||||
for (let i = 1; i < s.length; i++) {
|
||||
const dt = (s[i].t - s[i - 1].t) / 1000
|
||||
const dx = Math.abs(s[i].x - s[i - 1].x)
|
||||
if (dt > 0 && s[i].ground) best = Math.max(best, dx / dt)
|
||||
}
|
||||
return Math.round(best)
|
||||
}
|
||||
console.log('[feel] A 站立满跳:', JSON.stringify(analyze(aS, 'A')))
|
||||
console.log('[feel] B 走极速:', topSpeed(walkS), 'px/s(采样口径)')
|
||||
console.log('[feel] B 跑极速:', topSpeed(runS), 'px/s(采样口径)')
|
||||
console.log('[feel] C 连跳:', JSON.stringify(analyze(cS, 'C')))
|
||||
console.log('[feel] D 按住不放:', JSON.stringify(analyze(dS, 'D')))
|
||||
const report = await js(`({ marioErr: window.__marioErr ?? null, pageErrors: window.__pageErrors || [] })`)
|
||||
console.log('[feel] 异常:', JSON.stringify(report), 'console:', pageErrors.length)
|
||||
app.exit(0)
|
||||
}
|
||||
app.whenReady().then(() => main().catch((e) => { console.error('[feel] 失败:', e.message); app.exit(1) }))
|
||||
setTimeout(() => { console.error('[feel] 总超时'); app.exit(1) }, 90000)
|
||||
188
electron/mario-verify.cjs
Normal file
188
electron/mario-verify.cjs
Normal file
@@ -0,0 +1,188 @@
|
||||
// 马里奥专项验证(开发用临时脚本):
|
||||
// 1. 登录 → 解锁全部关卡(API)→ 1-1 开局:横幅/问号居中/跑动截图
|
||||
// 2. 依次冒烟 2-1/2-2/2-3/2-4:开局跑动,检查循环异常与 AudioContext
|
||||
// 运行:NLG_E2E_MODE=web npx electron electron/mario-verify.cjs(需 5173 开发服务器)
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const WEB_URL = 'http://localhost:5173'
|
||||
const OUT_DIR = process.cwd()
|
||||
const pageErrors = []
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
async function main() {
|
||||
const win = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 860,
|
||||
show: true,
|
||||
backgroundColor: '#0f1220',
|
||||
webPreferences: { backgroundThrottling: false },
|
||||
})
|
||||
win.webContents.setBackgroundThrottling(false)
|
||||
win.webContents.on('console-message', (e) => {
|
||||
if (e?.level === 'error' && e?.message) pageErrors.push(e.message)
|
||||
})
|
||||
const js = (code) => win.webContents.executeJavaScript(code, true).catch((e) => {
|
||||
console.error('[verify] JS 执行失败片段:', JSON.stringify(code.slice(0, 100).replace(/\s+/g, ' ')))
|
||||
throw e
|
||||
})
|
||||
const shot = async (name) => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
const img = await win.webContents.capturePage()
|
||||
const file = path.join(OUT_DIR, `${name}.png`)
|
||||
fs.writeFileSync(file, img.toPNG())
|
||||
console.log('[verify] screenshot:', file)
|
||||
return
|
||||
} catch (err) {
|
||||
if (i === 2) console.warn(`[verify] 截图 ${name} 失败:${err.message}`)
|
||||
else await sleep(600)
|
||||
}
|
||||
}
|
||||
}
|
||||
const waitFor = async (cond, timeoutMs, desc) => {
|
||||
const t0 = Date.now()
|
||||
while (Date.now() - t0 < timeoutMs) {
|
||||
try { if (await js(cond)) return } catch {}
|
||||
await sleep(300)
|
||||
}
|
||||
throw new Error(`等待超时:${desc}`)
|
||||
}
|
||||
const runRight = async (ms) => {
|
||||
await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); true`)
|
||||
await sleep(ms)
|
||||
await js(`window.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' })); true`)
|
||||
}
|
||||
|
||||
await win.loadURL(WEB_URL)
|
||||
await js('localStorage.clear(); true')
|
||||
await win.webContents.reload()
|
||||
await waitFor(`!!document.querySelector('input[placeholder^="用户名"]')`, 10000, '登录页渲染')
|
||||
await js(`
|
||||
(() => {
|
||||
const us = document.querySelector('input[placeholder^="用户名"]')
|
||||
us.value = 'mariotest01'; us.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
const ps = document.querySelector('input[type="password"]')
|
||||
ps.value = 'test123456'; ps.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
document.querySelector('button.submit').click()
|
||||
return true
|
||||
})()
|
||||
`)
|
||||
await waitFor(`!!localStorage.getItem('token')`, 8000, '登录')
|
||||
console.log('[verify] 登录成功')
|
||||
|
||||
// 解锁全部 8 关(后端防作弊每次最多 +1,循环推进)
|
||||
await js(`
|
||||
(async () => {
|
||||
const H = { Authorization: 'Bearer ' + localStorage.getItem('token'), 'Content-Type': 'application/json' }
|
||||
for (let lv = 2; lv <= 12; lv++) {
|
||||
for (let t = 0; t < 3; t++) {
|
||||
try {
|
||||
const r = await fetch('/api/games/mario/progress', {
|
||||
method: 'POST', headers: H, body: JSON.stringify({ level: lv }),
|
||||
})
|
||||
const j = await r.json()
|
||||
if (j && j.code === 0) break
|
||||
} catch {}
|
||||
await new Promise((r2) => setTimeout(r2, 400))
|
||||
}
|
||||
}
|
||||
const g = await fetch('/api/games/mario/progress', { headers: { Authorization: H.Authorization } }).then((r) => r.json())
|
||||
return (g.data || {}).level
|
||||
})()
|
||||
`)
|
||||
await sleep(800)
|
||||
const unlockedLevel = await js(`fetch('/api/games/mario/progress', { headers: { Authorization: 'Bearer ' + localStorage.getItem('token') } }).then((r) => r.json()).then((j) => (j.data || {}).level).catch(() => 0)`)
|
||||
console.log('[verify] 解锁进度:', unlockedLevel)
|
||||
|
||||
const playLevel = async (label, idx, runMs) => {
|
||||
await js(`location.href = '/play/mario'; true`).catch(() => {})
|
||||
try {
|
||||
await waitFor(`!![...document.querySelectorAll('button')].find((b) => /开始闯关|继续第|开始游戏/.test(b.textContent))`, 20000, `${label} 游玩页`)
|
||||
} catch (e) {
|
||||
await shot(`mario-verify-debug-${idx}`)
|
||||
console.error('[verify] 页面状态:', JSON.stringify(await js(`({ url: location.href, btns: [...document.querySelectorAll('button')].map((b) => b.textContent.trim()).slice(0, 12) })`)))
|
||||
throw e
|
||||
}
|
||||
// 开局前装监听 + 代理 AudioContext
|
||||
await js(`
|
||||
(() => {
|
||||
window.__pageErrors = []
|
||||
window.addEventListener('error', (e) => window.__pageErrors.push(String(e.message || e)))
|
||||
window.addEventListener('unhandledrejection', (e) => window.__pageErrors.push(String(e.reason)))
|
||||
const Orig = window.AudioContext || window.webkitAudioContext
|
||||
window.AudioContext = class extends Orig {
|
||||
constructor(...a) { super(...a); (window.__actxs = window.__actxs || []).push(this) }
|
||||
}
|
||||
// 选择关卡(若按钮可用)再开始
|
||||
const btns = [...document.querySelectorAll('button')]
|
||||
const lv = btns.find((b) => b.textContent.includes('世界 ${label}') && !b.disabled)
|
||||
if (lv) lv.click()
|
||||
return true
|
||||
})()
|
||||
`)
|
||||
await sleep(300)
|
||||
await js(`
|
||||
(() => {
|
||||
const b = document.querySelector('.overlay .ls-continue') ||
|
||||
[...document.querySelectorAll('button')].find((x) => /开始闯关|继续第|开始游戏/.test(x.textContent))
|
||||
if (!b) return false
|
||||
b.click()
|
||||
return true
|
||||
})()
|
||||
`)
|
||||
await sleep(900)
|
||||
if (idx === 'a') await shot('mario-verify-select') // 世界选关画面
|
||||
// 光标默认停在最新解锁关(11=3-4),用右移导航到目标关:(i+1)%12 次到 i
|
||||
const target = { a: 0, b: 4, c: 8, d: 9, e: 10, f: 11 }[idx] ?? 11
|
||||
const presses = (target + 1) % 12
|
||||
for (let i = 0; i < presses; i++) {
|
||||
await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); true`)
|
||||
await sleep(50)
|
||||
await js(`window.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' })); true`)
|
||||
await sleep(60)
|
||||
}
|
||||
await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); true`)
|
||||
await sleep(80)
|
||||
await js(`window.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' })); true`)
|
||||
await sleep(3400) // 横幅 1.6s + 余量
|
||||
await runRight(runMs)
|
||||
await sleep(1600)
|
||||
const report = await js(`({
|
||||
marioErr: window.__marioErr ?? null,
|
||||
pageErrors: window.__pageErrors,
|
||||
audio: (window.__actxs || []).map((a) => a.state),
|
||||
level: window.__marioLevel ?? null,
|
||||
})`)
|
||||
const wantLevel = { a: '1-1', b: '2-1', c: '3-1', d: '3-2', e: '3-3', f: '3-4' }[idx]
|
||||
if (report.level !== wantLevel) {
|
||||
console.error(`[verify] ${label} 未进入目标关:期望 ${wantLevel},实际 ${report.level}`)
|
||||
report.marioErr = report.marioErr || 'level mismatch'
|
||||
}
|
||||
console.log(`[verify] ${label} report:`, JSON.stringify(report))
|
||||
await shot(`mario-verify-${idx}-${label}`)
|
||||
return report
|
||||
}
|
||||
|
||||
const reports = []
|
||||
reports.push(await playLevel('1-1', 'a', 1400))
|
||||
for (const [label, idx, runMs] of [['2-1', 'b', 3200], ['3-1', 'c', 3000], ['3-2', 'd', 2600], ['3-3', 'e', 2400], ['3-4', 'f', 2400]]) {
|
||||
reports.push(await playLevel(label, idx, runMs))
|
||||
}
|
||||
|
||||
const bad = reports.filter((r) => r.marioErr || r.pageErrors.length)
|
||||
const audioOk = reports.filter((r) => r.audio.includes('running')).length >= 2
|
||||
if (pageErrors.length) console.error('[verify] 页面 console 错误:', pageErrors.slice(0, 8))
|
||||
if (bad.length === 0 && audioOk && pageErrors.length === 0) {
|
||||
console.log('[verify] PASS:五关全部正常,无循环异常,BGM 引擎运行')
|
||||
} else {
|
||||
console.error('[verify] FAIL', JSON.stringify({ bad, audioOk }))
|
||||
app.exit(1)
|
||||
return
|
||||
}
|
||||
app.exit(0)
|
||||
}
|
||||
|
||||
app.whenReady().then(() => main().catch((err) => { console.error('[verify] 失败:', err.message); app.exit(1) }))
|
||||
setTimeout(() => { console.error('[verify] 总超时'); app.exit(1) }, 180000)
|
||||
@@ -14,6 +14,14 @@ contextBridge.exposeInMainWorld('desktop', {
|
||||
checkUpdate: (silent = false) => ipcRenderer.invoke('nlg:check-update', silent),
|
||||
// 取消正在进行的更新下载
|
||||
cancelUpdateDownload: () => ipcRenderer.invoke('nlg:cancel-update-download'),
|
||||
// 切换屏幕模式:window 普通窗口 / windowFull 窗口全屏 / full 真全屏
|
||||
setScreenMode: (mode) => ipcRenderer.invoke('nlg:set-screen-mode', mode),
|
||||
// 订阅屏幕模式变化(Esc 退全屏、菜单 F11 等原生状态变化时同步),返回取消订阅函数
|
||||
onScreenModeChanged: (cb) => {
|
||||
const handler = (_e, mode) => cb(mode)
|
||||
ipcRenderer.on('nlg:screen-mode-changed', handler)
|
||||
return () => ipcRenderer.removeListener('nlg:screen-mode-changed', handler)
|
||||
},
|
||||
// 运行环境版本信息
|
||||
versions: {
|
||||
electron: process.versions.electron,
|
||||
|
||||
Reference in New Issue
Block a user