From bed6b475a30d8469f8a7f4c34019d873ab8475c3 Mon Sep 17 00:00:00 2001 From: Rin Date: Mon, 9 Mar 2026 22:06:13 +0800 Subject: [PATCH] fix(stage-tamagotchi): bring back onboarding dialog (#1104) --- apps/stage-tamagotchi/src/main/index.ts | 9 ++ .../src/main/windows/onboarding/index.ts | 117 ++++++++++++++++++ .../src/renderer/pages/onboarding.vue | 56 +++++++-- apps/stage-tamagotchi/src/shared/eventa.ts | 6 + 4 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 apps/stage-tamagotchi/src/main/windows/onboarding/index.ts diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 56d3a3e4d..5cb7a728c 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -31,6 +31,7 @@ import { setupChatWindowReusableFunc } from './windows/chat' import { setupDevtoolsWindow } from './windows/devtools' import { setupMainWindow } from './windows/main' import { setupNoticeWindowManager } from './windows/notice' +import { setupOnboardingWindowManager } from './windows/onboarding' import { setupSettingsWindowReusableFunc } from './windows/settings' import { setupWidgetsWindowManager } from './windows/widgets' @@ -107,6 +108,8 @@ app.whenReady().then(async () => { const beatSync = injeca.provide('windows:beat-sync', () => setupBeatSync()) const devtoolsMarkdownStressWindow = injeca.provide('windows:devtools:markdown-stress', () => setupDevtoolsWindow()) + + const onboardingWindow = injeca.provide('windows:onboarding', () => setupOnboardingWindowManager()) const noticeWindow = injeca.provide('windows:notice', { dependsOn: { i18n, serverChannel }, build: ({ dependsOn }) => setupNoticeWindowManager(dependsOn), @@ -152,6 +155,12 @@ app.whenReady().then(async () => { callback: noop, }) + // Show onboarding window if not completed + injeca.invoke({ + dependsOn: { onboardingWindow }, + callback: ({ onboardingWindow }) => { onboardingWindow.showIfNeeded() }, + }) + injeca.start().catch(err => console.error(err)) // Lifecycle diff --git a/apps/stage-tamagotchi/src/main/windows/onboarding/index.ts b/apps/stage-tamagotchi/src/main/windows/onboarding/index.ts new file mode 100644 index 000000000..1bd7a7a13 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/windows/onboarding/index.ts @@ -0,0 +1,117 @@ +import { join, resolve } from 'node:path' + +import { is } from '@electron-toolkit/utils' +import { defineInvokeHandler } from '@moeru/eventa' +import { createContext } from '@moeru/eventa/adapters/electron/main' +import { BrowserWindow, ipcMain, shell } from 'electron' + +import icon from '../../../../resources/icon.png?asset' + +import { electronOnboardingClose, electronOnboardingCompleted, electronOnboardingSkipped, electronOpenOnboarding } from '../../../shared/eventa' +import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location' + +export interface OnboardingWindowManager { + getWindow: () => BrowserWindow | null + showIfNeeded: () => Promise + close: () => void + markCompleted: () => void + markSkipped: () => void +} + +export function setupOnboardingWindowManager(): OnboardingWindowManager { + let window: BrowserWindow | undefined + + const close = () => { + if (window && !window.isDestroyed()) { + window.close() + } + } + + const markCompleted = () => { + close() + } + + const markSkipped = () => { + close() + } + + const createWindow = async () => { + if (window && !window.isDestroyed()) { + return window + } + + const newWindow = new BrowserWindow({ + title: 'Welcome to AIRI', + width: 500, + height: 700, + minWidth: 400, + minHeight: 500, + show: false, + icon, + resizable: true, + frame: false, + titleBarStyle: 'hidden', + transparent: false, + backgroundColor: '#0f0f0f', + webPreferences: { + preload: join(getElectronMainDirname(), '../preload/index.mjs'), + sandbox: false, + }, + }) + + // NOTICE: in development mode, open devtools by default + if (is.dev) { + try { + newWindow.webContents.openDevTools({ mode: 'detach' }) + } + catch (err) { + console.error('failed to open devtools:', err) + } + } + + newWindow.on('ready-to-show', () => newWindow.show()) + newWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + newWindow.on('closed', () => { + window = undefined + }) + + await load(newWindow, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/onboarding')) + + // Setup IPC handlers for this window + const { context } = createContext(ipcMain, newWindow) + + defineInvokeHandler(context, electronOnboardingClose, () => close()) + defineInvokeHandler(context, electronOnboardingCompleted, () => markCompleted()) + defineInvokeHandler(context, electronOnboardingSkipped, () => markSkipped()) + defineInvokeHandler(context, electronOpenOnboarding, async () => { + await showIfNeeded() + return true + }) + + window = newWindow + return newWindow + } + + async function showIfNeeded() { + if (window && !window.isDestroyed()) { + window.show() + window.focus() + return true + } + + await createWindow() + return true + } + + return { + getWindow: () => window ?? null, + showIfNeeded, + close, + markCompleted, + markSkipped, + } +} diff --git a/apps/stage-tamagotchi/src/renderer/pages/onboarding.vue b/apps/stage-tamagotchi/src/renderer/pages/onboarding.vue index 3f004e066..de7887ef5 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/onboarding.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/onboarding.vue @@ -1,31 +1,69 @@ + + meta: layout: plain diff --git a/apps/stage-tamagotchi/src/shared/eventa.ts b/apps/stage-tamagotchi/src/shared/eventa.ts index 23f129778..5dac87938 100644 --- a/apps/stage-tamagotchi/src/shared/eventa.ts +++ b/apps/stage-tamagotchi/src/shared/eventa.ts @@ -206,6 +206,12 @@ export const widgetsRemoveEvent = defineEventa<{ id: string }>('eventa:event:ele export const widgetsClearEvent = defineEventa('eventa:event:electron:windows:widgets:clear') export const widgetsUpdateEvent = defineEventa<{ id: string, componentProps?: Record }>('eventa:event:electron:windows:widgets:update') +// Onboarding window events +export const electronOnboardingClose = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:close') +export const electronOnboardingCompleted = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:completed') +export const electronOnboardingSkipped = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:skipped') +export const electronOpenOnboarding = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:open') + export const i18nSetLocale = defineInvokeEventa('eventa:invoke:electron:i18n:set-locale') export const i18nGetLocale = defineInvokeEventa('eventa:invoke:electron:i18n:get-locale')