**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>
97 lines
3.5 KiB
TypeScript
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()
|
|
}
|
|
}
|