refactor(stage-ui,stage-tamagotchi): better structure, better injeca
This commit is contained in:
@@ -1,33 +1,27 @@
|
||||
import type { BrowserWindow } from 'electron'
|
||||
|
||||
import type { WidgetsWindowManager } from './windows/widgets'
|
||||
|
||||
import { env, platform } from 'node:process'
|
||||
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg'
|
||||
import { app, ipcMain, Menu, nativeImage, Tray } from 'electron'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { initMain as initAudioLoopback } from 'electron-audio-loopback'
|
||||
import { noop, once } from 'es-toolkit'
|
||||
import { noop } from 'es-toolkit'
|
||||
import { createLoggLogger, injeca } from 'injeca'
|
||||
import { isLinux, isMacOS } from 'std-env'
|
||||
import { isLinux } from 'std-env'
|
||||
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import macOSTrayIcon from '../../resources/tray-icon-macos.png?asset'
|
||||
|
||||
import { openDebugger, setupDebugger } from './app/debugger'
|
||||
import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed, onAppBeforeQuit } from './libs/bootkit/lifecycle'
|
||||
import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed } from './libs/bootkit/lifecycle'
|
||||
import { setElectronMainDirname } from './libs/electron/location'
|
||||
import { setupChannelServer } from './services/airi/channel-server'
|
||||
import { setupServerChannel } from './services/airi/channel-server'
|
||||
import { setupTray } from './tray'
|
||||
import { setupAboutWindowReusable } from './windows/about'
|
||||
import { setupBeatSync } from './windows/beat-sync'
|
||||
import { setupCaptionWindowManager } from './windows/caption'
|
||||
import { setupChatWindowReusableFunc } from './windows/chat'
|
||||
import { setupInlayWindow } from './windows/inlay'
|
||||
import { setupMainWindow } from './windows/main'
|
||||
import { setupNoticeWindowManager } from './windows/notice'
|
||||
import { setupSettingsWindowReusableFunc } from './windows/settings'
|
||||
import { toggleWindowShow } from './windows/shared/window'
|
||||
import { setupWidgetsWindowManager } from './windows/widgets'
|
||||
|
||||
// TODO: once we refactored eventa to support window-namespaced contexts,
|
||||
@@ -73,70 +67,16 @@ electronApp.setAppUserModelId('ai.moeru.airi')
|
||||
|
||||
initAudioLoopback()
|
||||
|
||||
function setupTray(params: {
|
||||
mainWindow: BrowserWindow
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
captionWindow: ReturnType<typeof setupCaptionWindowManager>
|
||||
widgetsWindow: WidgetsWindowManager
|
||||
beatSyncBgWindow: BrowserWindow
|
||||
aboutWindow: () => Promise<BrowserWindow>
|
||||
}): void {
|
||||
once(() => {
|
||||
const trayImage = nativeImage.createFromPath(isMacOS ? macOSTrayIcon : icon).resize({ width: 16 })
|
||||
trayImage.setTemplateImage(isMacOS)
|
||||
const appTray = new Tray(trayImage)
|
||||
onAppBeforeQuit(() => appTray.destroy())
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Show', click: () => toggleWindowShow(params.mainWindow) },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Settings...', click: () => params.settingsWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: 'About...', click: () => params.aboutWindow().then(window => toggleWindowShow(window)) },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Open Inlay...', click: () => setupInlayWindow() },
|
||||
{ label: 'Open Widgets...', click: () => params.widgetsWindow.getWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: 'Open Caption...', click: () => params.captionWindow.getWindow().then(window => toggleWindowShow(window)) },
|
||||
{
|
||||
type: 'submenu',
|
||||
label: 'Caption Overlay',
|
||||
submenu: Menu.buildFromTemplate([
|
||||
{ type: 'checkbox', label: 'Follow window', checked: params.captionWindow.getIsFollowingWindow(), click: async menuItem => await params.captionWindow.setFollowWindow(Boolean(menuItem.checked)) },
|
||||
{ label: 'Reset position', click: async () => await params.captionWindow.resetToSide() },
|
||||
]),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
...is.dev || env.MAIN_APP_DEBUG || env.APP_DEBUG
|
||||
? [
|
||||
{ type: 'header', label: 'DevTools' },
|
||||
{ label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBgWindow.webContents.openDevTools() },
|
||||
{ type: 'separator' },
|
||||
] as const // :(
|
||||
: [],
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
])
|
||||
|
||||
appTray.setContextMenu(contextMenu)
|
||||
appTray.setToolTip('Project AIRI')
|
||||
appTray.addListener('click', () => toggleWindowShow(params.mainWindow))
|
||||
|
||||
// On macOS, there's a special double-click event
|
||||
if (isMacOS)
|
||||
appTray.addListener('double-click', () => toggleWindowShow(params.mainWindow))
|
||||
})()
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
injeca.setLogger(createLoggLogger(useLogg('injeca').useGlobalConfig()))
|
||||
|
||||
const channelServerModule = injeca.provide('modules:channel-server', async () => setupChannelServer())
|
||||
const widgetsManager = injeca.provide('windows:widgets', { build: () => setupWidgetsWindowManager() })
|
||||
const noticeWindow = injeca.provide('windows:notice', { build: () => setupNoticeWindowManager() })
|
||||
const aboutWindow = injeca.provide('windows:about', { build: () => setupAboutWindowReusable() })
|
||||
const serverChannel = injeca.provide('modules:channel-server', () => setupServerChannel())
|
||||
const widgetsManager = injeca.provide('windows:widgets', () => setupWidgetsWindowManager())
|
||||
const noticeWindow = injeca.provide('windows:notice', () => setupNoticeWindowManager())
|
||||
const aboutWindow = injeca.provide('windows:about', () => setupAboutWindowReusable())
|
||||
|
||||
// BeatSync will create a background window to capture and process audio.
|
||||
const beatSync = injeca.provide('windows:beat-sync', {
|
||||
build: () => setupBeatSync(),
|
||||
})
|
||||
const beatSync = injeca.provide('windows:beat-sync', () => setupBeatSync())
|
||||
|
||||
const chatWindow = injeca.provide('windows:chat', {
|
||||
dependsOn: { widgetsManager },
|
||||
@@ -152,6 +92,7 @@ app.whenReady().then(async () => {
|
||||
return window
|
||||
},
|
||||
})
|
||||
|
||||
const mainWindow = injeca.provide('windows:main', {
|
||||
dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync },
|
||||
build: async ({ dependsOn }) => {
|
||||
@@ -161,10 +102,12 @@ app.whenReady().then(async () => {
|
||||
return window
|
||||
},
|
||||
})
|
||||
|
||||
const captionWindow = injeca.provide('windows:caption', {
|
||||
dependsOn: { mainWindow },
|
||||
build: async ({ dependsOn }) => setupCaptionWindowManager(dependsOn),
|
||||
})
|
||||
|
||||
const tray = injeca.provide('app:tray', {
|
||||
dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager, beatSync, aboutWindow },
|
||||
build: async ({ dependsOn }) => {
|
||||
@@ -172,8 +115,9 @@ app.whenReady().then(async () => {
|
||||
return setupTray({ ...setupArgs, beatSyncBgWindow: beatSync.window, aboutWindow })
|
||||
},
|
||||
})
|
||||
|
||||
injeca.invoke({
|
||||
dependsOn: { mainWindow, tray, channelServerModule },
|
||||
dependsOn: { mainWindow, tray, serverChannel },
|
||||
callback: noop,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useLogg } from '@guiiai/logg'
|
||||
|
||||
import { onAppBeforeQuit } from '../../../libs/bootkit/lifecycle'
|
||||
|
||||
export async function setupChannelServer() {
|
||||
export async function setupServerChannel() {
|
||||
const log = useLogg('main/server-runtime').useGlobalConfig()
|
||||
|
||||
// Start the server-runtime server with WebSocket support
|
||||
@@ -22,14 +22,13 @@ export async function setupChannelServer() {
|
||||
port: env.PORT ? Number(env.PORT) : 6121,
|
||||
hostname: env.SERVER_RUNTIME_HOSTNAME || 'localhost',
|
||||
reusePort: true,
|
||||
silent: true,
|
||||
gracefulShutdown: {
|
||||
forceTimeout: 500,
|
||||
gracefulTimeout: 500,
|
||||
},
|
||||
})
|
||||
|
||||
log.log('@proj-airi/server-runtime started on ws://localhost:6121')
|
||||
|
||||
onAppBeforeQuit(async () => {
|
||||
if (serverInstance && typeof serverInstance.close === 'function') {
|
||||
try {
|
||||
@@ -41,6 +40,8 @@ export async function setupChannelServer() {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
log.log('@proj-airi/server-runtime started on ws://localhost:6121')
|
||||
}
|
||||
catch (error) {
|
||||
log.withError(error).error('failed to start WebSocket server')
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { BrowserWindow } from 'electron'
|
||||
|
||||
import type { setupCaptionWindowManager } from '../windows/caption'
|
||||
import type { WidgetsWindowManager } from '../windows/widgets'
|
||||
|
||||
import { env } from 'node:process'
|
||||
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { app, Menu, nativeImage, Tray } from 'electron'
|
||||
import { once } from 'es-toolkit'
|
||||
import { isMacOS } from 'std-env'
|
||||
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import macOSTrayIcon from '../../resources/tray-icon-macos.png?asset'
|
||||
|
||||
import { onAppBeforeQuit } from '../libs/bootkit/lifecycle'
|
||||
import { setupInlayWindow } from '../windows/inlay'
|
||||
import { toggleWindowShow } from '../windows/shared/window'
|
||||
|
||||
export function setupTray(params: {
|
||||
mainWindow: BrowserWindow
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
captionWindow: ReturnType<typeof setupCaptionWindowManager>
|
||||
widgetsWindow: WidgetsWindowManager
|
||||
beatSyncBgWindow: BrowserWindow
|
||||
aboutWindow: () => Promise<BrowserWindow>
|
||||
}): void {
|
||||
once(() => {
|
||||
const trayImage = nativeImage.createFromPath(isMacOS ? macOSTrayIcon : icon).resize({ width: 16 })
|
||||
trayImage.setTemplateImage(isMacOS)
|
||||
|
||||
const appTray = new Tray(trayImage)
|
||||
onAppBeforeQuit(() => appTray.destroy())
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Show', click: () => toggleWindowShow(params.mainWindow) },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Settings...', click: () => params.settingsWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: 'About...', click: () => params.aboutWindow().then(window => toggleWindowShow(window)) },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Open Inlay...', click: () => setupInlayWindow() },
|
||||
{ label: 'Open Widgets...', click: () => params.widgetsWindow.getWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: 'Open Caption...', click: () => params.captionWindow.getWindow().then(window => toggleWindowShow(window)) },
|
||||
{
|
||||
type: 'submenu',
|
||||
label: 'Caption Overlay',
|
||||
submenu: Menu.buildFromTemplate([
|
||||
{ type: 'checkbox', label: 'Follow window', checked: params.captionWindow.getIsFollowingWindow(), click: async menuItem => await params.captionWindow.setFollowWindow(Boolean(menuItem.checked)) },
|
||||
{ label: 'Reset position', click: async () => await params.captionWindow.resetToSide() },
|
||||
]),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
...is.dev || env.MAIN_APP_DEBUG || env.APP_DEBUG
|
||||
? [
|
||||
{ type: 'header', label: 'DevTools' },
|
||||
{ label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBgWindow.webContents.openDevTools() },
|
||||
{ type: 'separator' },
|
||||
] as const // :(
|
||||
: [],
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
])
|
||||
|
||||
appTray.setContextMenu(contextMenu)
|
||||
appTray.setToolTip('Project AIRI')
|
||||
appTray.addListener('click', () => toggleWindowShow(params.mainWindow))
|
||||
|
||||
// On macOS, there's a special double-click event
|
||||
if (isMacOS) {
|
||||
appTray.addListener('double-click', () => toggleWindowShow(params.mainWindow))
|
||||
}
|
||||
})()
|
||||
}
|
||||
@@ -51,7 +51,7 @@ vi.mock('./modules', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('./mods/api/channel-server', () => ({
|
||||
useModsChannelServerStore: () => ({
|
||||
useModsServerChannelStore: () => ({
|
||||
connected: ref(true),
|
||||
initialize: mockInitialize,
|
||||
onContextUpdate: (cb: typeof contextUpdateHandler) => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { useModsChannelServerStore } from './mods/api/channel-server'
|
||||
import { useModsServerChannelStore } from './mods/api/channel-server'
|
||||
|
||||
export const useConfiguratorByModsChannelServer = defineStore('configurator:adapter:proj-airi:server-sdk', () => {
|
||||
const { send } = useModsChannelServerStore()
|
||||
const { send } = useModsServerChannelStore()
|
||||
|
||||
function updateFor(moduleName: string, config: Record<string, unknown>) {
|
||||
send({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Client } from '@proj-airi/server-sdk'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useModsChannelServerStore = defineStore('mods:channels:proj-airi:server', () => {
|
||||
export const useModsServerChannelStore = defineStore('mods:channels:proj-airi:server', () => {
|
||||
const connected = ref(false)
|
||||
const client = ref<Client>()
|
||||
const initializing = ref<Promise<void> | null>(null)
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useBroadcastChannel } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { CONTEXT_CHANNEL_NAME, useChatStore } from '../chat'
|
||||
import { useModsChannelServerStore } from '../mods/api/channel-server'
|
||||
import { useModsServerChannelStore } from '../mods/api/channel-server'
|
||||
|
||||
let installed = false
|
||||
|
||||
@@ -18,7 +18,7 @@ export function installChatContextBridge() {
|
||||
}
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const modsChannelServer = useModsChannelServerStore()
|
||||
const modsChannelServer = useModsServerChannelStore()
|
||||
const { post: broadcastContext, data: incomingContext } = useBroadcastChannel<ContextMessage<ContextPayload>, ContextMessage<ContextPayload>>({
|
||||
name: CONTEXT_CHANNEL_NAME,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user