Skip to content

Commit 69cb613

Browse files
committed
feat(stage-tamagotchi): basic widget window manager, moved partial code from component-calling
1 parent 6db6542 commit 69cb613

21 files changed

Lines changed: 1214 additions & 19 deletions

File tree

apps/stage-tamagotchi/src/main/index.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { BrowserWindow } from 'electron'
22

3+
import type { WidgetsWindowManager } from './windows/widgets'
4+
35
import { platform } from 'node:process'
46

57
import { electronApp, optimizer } from '@electron-toolkit/utils'
@@ -22,6 +24,7 @@ import { setupInlayWindow } from './windows/inlay'
2224
import { setupMainWindow } from './windows/main'
2325
import { setupSettingsWindowReusableFunc } from './windows/settings'
2426
import { toggleWindowShow } from './windows/shared/window'
27+
import { setupWidgetsWindowManager } from './windows/widgets'
2528

2629
// TODO: once we refactored eventa to support window-namespaced contexts,
2730
// we can remove the setMaxListeners call below since eventa will be able to dispatch and
@@ -56,6 +59,7 @@ function setupTray(params: {
5659
mainWindow: BrowserWindow
5760
settingsWindow: () => Promise<BrowserWindow>
5861
captionWindow: ReturnType<typeof setupCaptionWindowManager>
62+
widgetsWindow: WidgetsWindowManager
5963
}): void {
6064
once(() => {
6165
const trayImage = nativeImage.createFromPath(isMacOS ? macOSTrayIcon : icon).resize({ width: 16 })
@@ -69,6 +73,7 @@ function setupTray(params: {
6973
{ label: 'Settings...', click: () => params.settingsWindow().then(window => toggleWindowShow(window)) },
7074
{ type: 'separator' },
7175
{ label: 'Open Inlay...', click: () => setupInlayWindow() },
76+
{ label: 'Open Widgets...', click: () => params.widgetsWindow.getWindow().then(window => toggleWindowShow(window)) },
7277
{ label: 'Open Caption...', click: () => params.captionWindow.getWindow().then(window => toggleWindowShow(window)) },
7378
{
7479
type: 'submenu',
@@ -96,18 +101,23 @@ app.whenReady().then(async () => {
96101
injecta.setLogger(createLoggLogger(useLogg('injecta').useGlobalConfig()))
97102

98103
const channelServerModule = injecta.provide('modules:channel-server', async () => setupChannelServer())
99-
const settingsWindow = injecta.provide('windows:settings', () => setupSettingsWindowReusableFunc())
100104
const chatWindow = injecta.provide('windows:chat', { build: () => setupChatWindowReusableFunc() })
105+
const widgetsManager = injecta.provide('windows:widgets', { build: () => setupWidgetsWindowManager() })
106+
107+
const settingsWindow = injecta.provide('windows:settings', {
108+
dependsOn: { widgetsManager },
109+
build: ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn),
110+
})
101111
const mainWindow = injecta.provide('windows:main', {
102-
dependsOn: { settingsWindow, chatWindow },
112+
dependsOn: { settingsWindow, chatWindow, widgetsManager },
103113
build: async ({ dependsOn }) => setupMainWindow(dependsOn),
104114
})
105115
const captionWindow = injecta.provide('windows:caption', {
106116
dependsOn: { mainWindow },
107117
build: async ({ dependsOn }) => setupCaptionWindowManager(dependsOn),
108118
})
109119
const tray = injecta.provide('app:tray', {
110-
dependsOn: { mainWindow, settingsWindow, captionWindow },
120+
dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager },
111121
build: async ({ dependsOn }) => setupTray(dependsOn),
112122
})
113123
injecta.invoke({

apps/stage-tamagotchi/src/main/libs/electron/window-manager/reusable.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,34 @@ import type { BrowserWindow } from 'electron'
22

33
export function createReusableWindow(setupFn: () => BrowserWindow | Promise<BrowserWindow>): { getWindow: () => Promise<BrowserWindow> } {
44
let window: BrowserWindow | undefined
5+
let windowSetupFnPromise: Promise<BrowserWindow> | undefined
56

6-
return {
7-
getWindow: async () => {
8-
if (!window) {
9-
window = await setupFn()
10-
return window
11-
}
12-
if (window.isDestroyed()) {
13-
window = await setupFn()
14-
return window
15-
}
16-
7+
const ensureWindow = async () => {
8+
if (window && !window.isDestroyed())
179
return window
18-
},
10+
11+
if (windowSetupFnPromise)
12+
return windowSetupFnPromise
13+
14+
windowSetupFnPromise = Promise.resolve(setupFn()).then((created) => {
15+
window = created
16+
windowSetupFnPromise = undefined
17+
18+
created.on?.('closed', () => {
19+
if (window === created)
20+
window = undefined
21+
})
22+
23+
return created
24+
}).catch((error) => {
25+
windowSetupFnPromise = undefined
26+
throw error
27+
})
28+
29+
return windowSetupFnPromise
30+
}
31+
32+
return {
33+
getWindow: async () => ensureWindow(),
1934
}
2035
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { createContext } from '@unbird/eventa/adapters/electron/main'
2+
import type { BrowserWindow, IpcMainEvent } from 'electron'
3+
4+
import type { WidgetsWindowManager } from '../../../windows/widgets'
5+
6+
import { defineInvokeHandlers } from '@unbird/eventa'
7+
8+
import { widgetsAdd, widgetsClear, widgetsFetch, widgetsOpenWindow, widgetsPrepareWindow, widgetsRemove, widgetsUpdate } from '../../../../shared/eventa'
9+
10+
interface InvokeOptions {
11+
raw?: { ipcMainEvent?: IpcMainEvent }
12+
}
13+
14+
function isFromWindow(options: InvokeOptions | undefined, window: BrowserWindow) {
15+
const sender = options?.raw?.ipcMainEvent?.sender
16+
if (!sender)
17+
return false
18+
return sender.id === window.webContents.id
19+
}
20+
21+
export function createWidgetsService(params: { context: ReturnType<typeof createContext>['context'], widgetsManager: WidgetsWindowManager, window: BrowserWindow }) {
22+
defineInvokeHandlers(params.context, {
23+
widgetsPrepareWindow,
24+
widgetsOpenWindow,
25+
widgetsAdd,
26+
widgetsUpdate,
27+
widgetsRemove,
28+
widgetsClear,
29+
widgetsFetch,
30+
}, {
31+
widgetsPrepareWindow: async (payload, options) => {
32+
if (!isFromWindow(options as InvokeOptions, params.window))
33+
return undefined
34+
return params.widgetsManager!.prepareWidgetWindow(payload ?? undefined)
35+
},
36+
widgetsOpenWindow: async (payload, options) => {
37+
if (!isFromWindow(options as InvokeOptions, params.window))
38+
return undefined
39+
return params.widgetsManager!.openWindow(payload ?? undefined)
40+
},
41+
widgetsAdd: async (payload, options) => {
42+
if (!isFromWindow(options as InvokeOptions, params.window))
43+
return undefined
44+
return payload ? params.widgetsManager!.pushWidget(payload) : undefined
45+
},
46+
widgetsUpdate: async (payload, options) => {
47+
if (!isFromWindow(options as InvokeOptions, params.window))
48+
return undefined
49+
return payload ? params.widgetsManager!.updateWidget(payload) : undefined
50+
},
51+
widgetsRemove: async (payload, options) => {
52+
if (!isFromWindow(options as InvokeOptions, params.window))
53+
return undefined
54+
return payload?.id ? params.widgetsManager!.removeWidget(payload.id) : undefined
55+
},
56+
widgetsClear: async (_payload, options) => {
57+
if (!isFromWindow(options as InvokeOptions, params.window))
58+
return undefined
59+
return params.widgetsManager!.clearWidgets()
60+
},
61+
widgetsFetch: async (payload, options) => {
62+
if (!isFromWindow(options as InvokeOptions, params.window))
63+
return undefined
64+
return payload?.id ? params.widgetsManager!.getWidgetSnapshot(payload.id) : undefined
65+
},
66+
})
67+
}

apps/stage-tamagotchi/src/main/windows/main/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { BrowserWindowConstructorOptions, Rectangle } from 'electron'
22

3+
import type { WidgetsWindowManager } from '../widgets'
4+
35
import { dirname, join, resolve } from 'node:path'
46
import { env } from 'node:process'
57
import { fileURLToPath } from 'node:url'
@@ -28,6 +30,7 @@ interface AppConfig {
2830
export async function setupMainWindow(params: {
2931
settingsWindow: () => Promise<BrowserWindow>
3032
chatWindow: () => Promise<BrowserWindow>
33+
widgetsManager: WidgetsWindowManager
3134
}) {
3235
const {
3336
setup: setupConfig,
@@ -123,7 +126,12 @@ export async function setupMainWindow(params: {
123126

124127
await load(window, baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')))
125128

126-
setupMainWindowElectronInvokes({ window, settingsWindow: params.settingsWindow, chatWindow: params.chatWindow })
129+
setupMainWindowElectronInvokes({
130+
window,
131+
settingsWindow: params.settingsWindow,
132+
chatWindow: params.chatWindow,
133+
widgetsManager: params.widgetsManager,
134+
})
127135

128136
/**
129137
* This is a know issue (or expected behavior maybe) to Electron.

apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import type { BrowserWindow } from 'electron'
22

3+
import type { WidgetsWindowManager } from '../../widgets'
4+
35
import { defineInvokeHandler } from '@unbird/eventa'
46
import { createContext } from '@unbird/eventa/adapters/electron/main'
57
import { ipcMain } from 'electron'
68

79
import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings } from '../../../../shared/eventa'
10+
import { createWidgetsService } from '../../../services/airi/widgets'
811
import { createScreenService, createWindowService } from '../../../services/electron'
912
import { toggleWindowShow } from '../../shared'
1013

1114
export function setupMainWindowElectronInvokes(params: {
1215
window: BrowserWindow
1316
settingsWindow: () => Promise<BrowserWindow>
1417
chatWindow: () => Promise<BrowserWindow>
18+
widgetsManager: WidgetsWindowManager
1519
}) {
1620
// TODO: once we refactored eventa to support window-namespaced contexts,
1721
// we can remove the setMaxListeners call below since eventa will be able to dispatch and
@@ -22,6 +26,7 @@ export function setupMainWindowElectronInvokes(params: {
2226

2327
createScreenService({ context, window: params.window })
2428
createWindowService({ context, window: params.window })
29+
createWidgetsService({ context, widgetsManager: params.widgetsManager, window: params.window })
2530

2631
defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' }))
2732
defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow()))

apps/stage-tamagotchi/src/main/windows/settings/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { WidgetsWindowManager } from '../widgets'
2+
13
import { join, resolve } from 'node:path'
24

35
import { BrowserWindow, shell } from 'electron'
@@ -8,7 +10,9 @@ import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs
810
import { createReusableWindow } from '../../libs/electron/window-manager'
911
import { setupSettingsWindowInvokes } from './rpc/index.electron'
1012

11-
export function setupSettingsWindowReusableFunc() {
13+
export function setupSettingsWindowReusableFunc(params: {
14+
widgetsManager: WidgetsWindowManager
15+
}) {
1216
return createReusableWindow(async () => {
1317
const window = new BrowserWindow({
1418
title: 'Settings',
@@ -29,7 +33,7 @@ export function setupSettingsWindowReusableFunc() {
2933
})
3034

3135
await load(window, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/settings'))
32-
await setupSettingsWindowInvokes({ settingsWindow: window })
36+
await setupSettingsWindowInvokes({ settingsWindow: window, widgetsManager: params.widgetsManager })
3337

3438
return window
3539
}).getWindow

apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type { BrowserWindow } from 'electron'
22

3+
import type { WidgetsWindowManager } from '../../widgets'
4+
35
import { defineInvokeHandler } from '@unbird/eventa'
46
import { createContext } from '@unbird/eventa/adapters/electron/main'
57
import { ipcMain } from 'electron'
68

79
import { electronOpenSettingsDevtools } from '../../../../shared/eventa'
10+
import { createWidgetsService } from '../../../services/airi/widgets'
811
import { createScreenService, createWindowService } from '../../../services/electron'
912

10-
export async function setupSettingsWindowInvokes(params: { settingsWindow: BrowserWindow }) {
13+
export async function setupSettingsWindowInvokes(params: { settingsWindow: BrowserWindow, widgetsManager: WidgetsWindowManager }) {
1114
// TODO: once we refactored eventa to support window-namespaced contexts,
1215
// we can remove the setMaxListeners call below since eventa will be able to dispatch and
1316
// manage events within eventa's context system.
@@ -17,6 +20,7 @@ export async function setupSettingsWindowInvokes(params: { settingsWindow: Brows
1720

1821
createScreenService({ context, window: params.settingsWindow })
1922
createWindowService({ context, window: params.settingsWindow })
23+
createWidgetsService({ context, widgetsManager: params.widgetsManager, window: params.settingsWindow })
2024

2125
defineInvokeHandler(context, electronOpenSettingsDevtools, async () => params.settingsWindow.webContents.openDevTools({ mode: 'detach' }))
2226
}

0 commit comments

Comments
 (0)