feat(stage-tamagotchi): dashboard window
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import type { Rectangle } from 'electron'
|
||||
import type { InferOutput } from 'valibot'
|
||||
|
||||
import type { NoticeWindowManager } from '../notice'
|
||||
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { env } from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import clickDragPlugin from 'electron-click-drag-plugin'
|
||||
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import { initScreenCaptureForWindow } from '@proj-airi/electron-screen-capture/main'
|
||||
import { defu } from 'defu'
|
||||
import { BrowserWindow, ipcMain, shell } from 'electron'
|
||||
import { isLinux } from 'std-env'
|
||||
import { array, number, object, optional, string } from 'valibot'
|
||||
|
||||
import icon from '../../../../resources/icon.png?asset'
|
||||
|
||||
import { electronStartDraggingWindow } from '../../../shared/eventa'
|
||||
import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location'
|
||||
import { createConfig } from '../../libs/electron/persistence'
|
||||
import { setupDashboardWindowElectronInvokes } from './rpc/index.electron'
|
||||
|
||||
const appConfigSchema = object({
|
||||
windows: optional(array(object({
|
||||
title: optional(string()),
|
||||
tag: string(),
|
||||
x: optional(number()),
|
||||
y: optional(number()),
|
||||
width: optional(number()),
|
||||
height: optional(number()),
|
||||
}))),
|
||||
})
|
||||
|
||||
type AppConfig = InferOutput<typeof appConfigSchema>
|
||||
|
||||
export async function setupDashboardWindow(params: {
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
noticeWindow: NoticeWindowManager
|
||||
onWindowCreated?: (window: BrowserWindow) => void
|
||||
}) {
|
||||
const {
|
||||
setup: setupConfig,
|
||||
get: getConfigRaw,
|
||||
update: updateConfig,
|
||||
} = createConfig('app', 'config.json', appConfigSchema, {
|
||||
default: { windows: [] },
|
||||
autoHeal: true,
|
||||
})
|
||||
const getConfig = (): AppConfig => getConfigRaw() ?? { windows: [] }
|
||||
|
||||
setupConfig()
|
||||
|
||||
const windowConfig = getConfig().windows?.find(w => w.title === 'AIRI Dashboard' && w.tag === 'dashboard')
|
||||
|
||||
const window = new BrowserWindow({
|
||||
title: 'AIRI Dashboard',
|
||||
width: windowConfig?.width ?? 1200.0,
|
||||
height: windowConfig?.height ?? 600.0,
|
||||
x: windowConfig?.x,
|
||||
y: windowConfig?.y,
|
||||
show: false,
|
||||
icon,
|
||||
webPreferences: {
|
||||
preload: join(dirname(fileURLToPath(import.meta.url)), '../preload/index.mjs'),
|
||||
sandbox: false,
|
||||
},
|
||||
})
|
||||
|
||||
if (params.onWindowCreated) {
|
||||
params.onWindowCreated(window)
|
||||
}
|
||||
|
||||
// NOTICE: in development mode, open devtools by default
|
||||
if (is.dev || env.MAIN_APP_DEBUG || env.APP_DEBUG) {
|
||||
try {
|
||||
window.webContents.openDevTools({ mode: 'detach' })
|
||||
}
|
||||
catch (err) {
|
||||
console.error('failed to open devtools:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function handleNewBounds(newBounds: Rectangle) {
|
||||
const config = getConfig()
|
||||
if (!config.windows || !Array.isArray(config.windows)) {
|
||||
config.windows = []
|
||||
}
|
||||
|
||||
const existingConfigIndex = config.windows.findIndex(w => w.title === 'AIRI Dashboard' && w.tag === 'dashboard')
|
||||
|
||||
if (existingConfigIndex === -1) {
|
||||
config.windows.push({
|
||||
title: 'AIRI Dashboard',
|
||||
tag: 'dashboard',
|
||||
x: newBounds.x,
|
||||
y: newBounds.y,
|
||||
width: newBounds.width,
|
||||
height: newBounds.height,
|
||||
})
|
||||
}
|
||||
else {
|
||||
const windowConfig = defu(config.windows[existingConfigIndex], { title: 'AIRI Dashboard', tag: 'dashboard' })
|
||||
|
||||
windowConfig.x = newBounds.x
|
||||
windowConfig.y = newBounds.y
|
||||
windowConfig.width = newBounds.width
|
||||
windowConfig.height = newBounds.height
|
||||
|
||||
config.windows[existingConfigIndex] = windowConfig
|
||||
}
|
||||
|
||||
updateConfig(config)
|
||||
}
|
||||
|
||||
window.on('resize', () => handleNewBounds(window.getBounds()))
|
||||
window.on('move', () => handleNewBounds(window.getBounds()))
|
||||
|
||||
window.on('ready-to-show', () => window!.show())
|
||||
window.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
await load(window, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/dashboard'))
|
||||
|
||||
setupDashboardWindowElectronInvokes({
|
||||
window,
|
||||
settingsWindow: params.settingsWindow,
|
||||
chatWindow: params.chatWindow,
|
||||
noticeWindow: params.noticeWindow,
|
||||
})
|
||||
|
||||
/**
|
||||
* This is a know issue (or expected behavior maybe) to Electron.
|
||||
* We don't use this approach on Linux because it's not working.
|
||||
*
|
||||
* Discussion: https://github.com/electron/electron/issues/37789
|
||||
* Workaround: https://github.com/noobfromph/electron-click-drag-plugin
|
||||
*/
|
||||
if (!isLinux) {
|
||||
function handleStartDraggingWindow() {
|
||||
try {
|
||||
const windowId = window.getNativeWindowHandle()
|
||||
clickDragPlugin.startDrag(windowId)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: once we refactored eventa to support window-namespaced contexts,
|
||||
// we can remove the setMaxListeners call below since eventa will be able to dispatch and
|
||||
// manage events within eventa's context system.
|
||||
ipcMain.setMaxListeners(0)
|
||||
|
||||
const { context } = createContext(ipcMain, window)
|
||||
const cleanUpWindowDraggingInvokeHandler = defineInvokeHandler(context, electronStartDraggingWindow, handleStartDraggingWindow)
|
||||
|
||||
window.on('closed', () => {
|
||||
cleanUpWindowDraggingInvokeHandler()
|
||||
})
|
||||
}
|
||||
|
||||
initScreenCaptureForWindow(window)
|
||||
|
||||
return window
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { BrowserWindow } from 'electron'
|
||||
|
||||
import type { NoticeWindowManager } from '../../notice'
|
||||
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings, noticeWindowEventa } from '../../../../shared/eventa'
|
||||
import { toggleWindowShow } from '../../shared'
|
||||
import { setupBaseWindowElectronInvokes } from '../../shared/window'
|
||||
|
||||
export function setupDashboardWindowElectronInvokes(params: {
|
||||
window: BrowserWindow
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
noticeWindow: NoticeWindowManager
|
||||
}) {
|
||||
// TODO: once we refactored eventa to support window-namespaced contexts,
|
||||
// we can remove the setMaxListeners call below since eventa will be able to dispatch and
|
||||
// manage events within eventa's context system.
|
||||
ipcMain.setMaxListeners(0)
|
||||
|
||||
const { context } = createContext(ipcMain, params.window)
|
||||
|
||||
setupBaseWindowElectronInvokes({ context, window: params.window })
|
||||
|
||||
defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' }))
|
||||
defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow()))
|
||||
defineInvokeHandler(context, electronOpenChat, async () => toggleWindowShow(await params.chatWindow()))
|
||||
defineInvokeHandler(context, noticeWindowEventa.openWindow, payload => params.noticeWindow.open(payload))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
Dashboard
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user