Files
warp-engine-client/src/main/ElectronApplication.ts
T
mr.zeroandClaude Opus 5 8511ccbef8
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
WarpEngine Client: the whole catalog, and a build that can point elsewhere
**The app is called WarpEngine Client.** "Store" named the thing it opens rather than the
thing you run, and the store is a catalog on a site, not a window on your machine. The
window title, the bundle, the packages and the menu entry follow; the repository already
did. The store being driven is named in the side menu, so the bar stopped repeating it as
a badge — the element stays in the page, hidden, because the window check reads it.

**Every title is listed, including the ones this machine cannot install.** They arrive
from the engine with `installable: false` and a reason, and they are drawn dimmed, with an
*unsupported platform* or *no build for this machine* badge, the engine's own sentence
underneath, and nothing to press: a disabled Install would invite a click that can never
work. They get a category of their own — *Not for this machine* — and they are kept out of
the native/hosted categories and counts, because a title with no build has no mode to be
counted under. An engine older than desktop 1.2.0 is unaffected: a missing `installable`
field reads as installable, which is what those engines mean.

**A build can be pointed at another site's registry:**

    make dist STORES_API=https://games.example.org/api/stores

BuildConfiguration reads the packaged package.json, where electron-builder's
extraMetadata writes that address, so a client for somebody else's catalog needs no source
change and nothing set on the user's machine. Precedence is runtime environment, then
build, then ours — three audiences, most specific first.

Also: the scrollbars are the window's own, because the platform's light track down the
side menu of a dark window looked like a mistake.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:45:54 +02:00

97 lines
3.5 KiB
TypeScript

import path from 'node:path'
import { BrowserWindow, dialog, type App, type IpcMain, type Shell } from 'electron'
import { ServiceContainer } from './composition/ServiceContainer'
import { MainWindowFactory } from './MainWindowFactory'
import { SelfTestRunner } from './diagnostics/SelfTestRunner'
const SELFTEST_FLAG = '--selftest'
const SELFTEST_USER_DATA_DIRECTORY = 'warpstore-gui-selftest'
const PRODUCT_NAME = 'WarpEngine Client'
/**
* The application's lifecycle.
*
* Thin on purpose: it owns the window and the process events, and hands everything
* else to the container. The self-test mode is part of the lifecycle because it has
* to bypass two of its rules — see below.
*/
export class ElectronApplication {
private readonly selfTest: boolean
private readonly container: ServiceContainer
private readonly windowFactory: MainWindowFactory
private window: BrowserWindow | null = null
public constructor (
private readonly app: App,
private readonly ipc: IpcMain,
shell: Shell,
argv: readonly string[] = process.argv
) {
this.selfTest = argv.includes(SELFTEST_FLAG)
this.container = new ServiceContainer(app, shell)
this.windowFactory = new MainWindowFactory((message: string, level: number): void => {
if (level >= 2 || this.selfTest) console.log(`[renderer] ${message}`)
})
}
public start (): void {
// A test run must never be swallowed by a copy the user already has open: it
// gets its own user-data directory and skips the single-instance lock. Without
// this the second process exits silently with status 0, which reads as a pass.
if (this.selfTest) {
this.app.setPath('userData', path.join(this.app.getPath('temp'), SELFTEST_USER_DATA_DIRECTORY))
} else if (!this.app.requestSingleInstanceLock()) {
this.app.quit()
return
}
this.container.registerIpc(this.ipc)
this.app.on('second-instance', (): void => { this.focusWindow() })
this.app.on('activate', (): void => {
if (BrowserWindow.getAllWindows().length === 0) this.openWindow()
})
this.app.on('window-all-closed', (): void => {
if (process.platform !== 'darwin') this.app.quit()
})
process.on('unhandledRejection', (reason: unknown): void => {
dialog.showErrorBox(PRODUCT_NAME, reason instanceof Error ? reason.message : String(reason))
})
void this.app.whenReady().then((): void => { this.openWindow() })
}
private openWindow (): void {
const window = this.windowFactory.createWindow()
this.window = window
this.container.streams.attachWindow(window)
window.on('closed', (): void => {
this.container.streams.detachWindow()
this.window = null
})
if (this.selfTest) this.scheduleSelfTest(window)
}
private scheduleSelfTest (window: BrowserWindow): void {
const runner = new SelfTestRunner(window)
window.webContents.once('did-finish-load', (): void => {
// The first listing has to finish before there is anything to look at.
setTimeout((): void => {
runner.run().then(
(passed: boolean): void => { this.app.exit(passed ? 0 : 1) },
(error: unknown): void => {
console.log(`SELFTEST ERROR ${error instanceof Error ? error.message : String(error)}`)
this.app.exit(1)
}
)
}, runner.settleDelayMs)
})
}
private focusWindow (): void {
const window = this.window
if (window === null) return
if (window.isMinimized()) window.restore()
window.focus()
}
}