From ecbc00972a9ffa835b7a54341b18b5330d725c9d Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Tue, 7 Oct 2025 00:36:16 +0800 Subject: [PATCH] feat(stage-tamagotchi): perserve window pos & size --- apps/stage-tamagotchi/package.json | 3 + .../src/main/windows/main/index.ts | 59 ++++++++++++++++++- .../src/main/windows/shared/persistence.ts | 49 +++++++++++++++ cspell.config.yaml | 1 + pnpm-lock.yaml | 9 +++ 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 apps/stage-tamagotchi/src/main/windows/shared/persistence.ts diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index e5bbc60b4..4940f8118 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -63,9 +63,12 @@ "colorjs.io": "^0.5.2", "culori": "^4.0.2", "date-fns": "^4.1.0", + "defu": "^6.1.4", + "destr": "^2.0.5", "dompurify": "^3.2.7", "drizzle-kit": "^0.31.5", "drizzle-orm": "^0.44.6", + "es-toolkit": "^1.39.10", "jszip": "^3.10.1", "listhen": "^1.9.0", "localforage": "^1.10.0", diff --git a/apps/stage-tamagotchi/src/main/windows/main/index.ts b/apps/stage-tamagotchi/src/main/windows/main/index.ts index 5a61016e0..7039b6443 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/index.ts @@ -1,22 +1,42 @@ +import type { BrowserWindowConstructorOptions, Rectangle } from 'electron' + import { dirname, join } from 'node:path' import { env } from 'node:process' import { fileURLToPath } from 'node:url' import { is } from '@electron-toolkit/utils' +import { defu } from 'defu' import { BrowserWindow, shell } from 'electron' import { isMacOS } from 'std-env' import icon from '../../../../resources/icon.png?asset' import { transparentWindowConfig } from '../shared' +import { createConfig } from '../shared/persistence' import { setupAppInvokeHandlers } from './eventa/index.electron' import { setupWebInvokes } from './eventa/index.web' +interface AppConfig { + windows?: Array & { tag: string }> +} + export function setup() { + const { + setup: setupConfig, + get: getConfig, + update: updateConfig, + } = createConfig('app', 'config.json', { default: { windows: [] } }) + + setupConfig() + + const mainWindowConfig = getConfig()?.windows?.find(w => w.title === 'AIRI' && w.tag === 'main') + const window = new BrowserWindow({ title: 'AIRI', - width: 450.0, - height: 600.0, + width: mainWindowConfig?.width ?? 450.0, + height: mainWindowConfig?.height ?? 600.0, + x: mainWindowConfig?.x, + y: mainWindowConfig?.y, show: false, icon, webPreferences: { @@ -26,6 +46,41 @@ export function setup() { ...transparentWindowConfig(), }) + 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' && w.tag === 'main') + + if (existingConfigIndex === -1) { + config.windows.push({ + title: 'AIRI', + tag: 'main', + x: newBounds.x, + y: newBounds.y, + width: newBounds.width, + height: newBounds.height, + }) + } + else { + const mainWindowConfig = defu(config.windows[existingConfigIndex], { title: 'AIRI', tag: 'main' }) + + mainWindowConfig.x = newBounds.x + mainWindowConfig.y = newBounds.y + mainWindowConfig.width = newBounds.width + mainWindowConfig.height = newBounds.height + + config.windows[existingConfigIndex] = mainWindowConfig + } + + updateConfig(config) + } + + window.on('resize', () => handleNewBounds(window.getBounds())) + window.on('move', () => handleNewBounds(window.getBounds())) + window.setAlwaysOnTop(true) if (isMacOS) { window.setWindowButtonVisibility(false) diff --git a/apps/stage-tamagotchi/src/main/windows/shared/persistence.ts b/apps/stage-tamagotchi/src/main/windows/shared/persistence.ts new file mode 100644 index 000000000..fc0a494df --- /dev/null +++ b/apps/stage-tamagotchi/src/main/windows/shared/persistence.ts @@ -0,0 +1,49 @@ +import { readFileSync } from 'node:fs' +import { writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +import { safeDestr } from 'destr' +import { app } from 'electron' +import { throttle } from 'es-toolkit' + +function parseOrFallback(config: string, fallback: T | undefined): T | undefined { + return safeDestr(config) || fallback +} + +const persistenceMap = new Map() + +export function createConfig(namespace: string, filename: string, options?: { default?: T }) { + function configPath() { + const path = join(app.getPath('userData'), `${namespace}-${filename}`) + return path + } + + function setup() { + const data = parseOrFallback(readFileSync(configPath(), { encoding: 'utf-8' }), options?.default) + persistenceMap.set(`${namespace}-${filename}`, data) + } + + const save = throttle(async () => { + try { + await writeFile(configPath(), JSON.stringify(persistenceMap.get(`${namespace}-${filename}`))) + } + catch (e) { + console.error('Failed to save config', e) + } + }, 250) + + function update(newData: T) { + persistenceMap.set(`${namespace}-${filename}`, newData) + save() + } + + function get(): T | undefined { + return persistenceMap.get(`${namespace}-${filename}`) as T | undefined + } + + return { + setup, + get, + update, + } +} diff --git a/cspell.config.yaml b/cspell.config.yaml index 768992ba1..b6ea29a6c 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -68,6 +68,7 @@ words: - defu - demi - demodel + - destr - devlog - devlogs - directml diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09bb9b334..18f5a61cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -356,6 +356,12 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + defu: + specifier: ^6.1.4 + version: 6.1.4 + destr: + specifier: ^2.0.5 + version: 2.0.5 dompurify: specifier: ^3.2.7 version: 3.2.7 @@ -365,6 +371,9 @@ importers: drizzle-orm: specifier: ^0.44.6 version: 0.44.6(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7) + es-toolkit: + specifier: ^1.39.10 + version: 1.39.10 jszip: specifier: ^3.10.1 version: 3.10.1