mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
feat: scaffold modern electron wrapper for scoreko nodecg bundle
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import { app, BrowserWindow, shell } from "electron";
|
||||
import { fork, ChildProcess } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
const APP_TITLE = "Scoreko";
|
||||
const DEFAULT_NODECG_PORT = process.env.NODECG_PORT ?? "9090";
|
||||
const DEFAULT_BUNDLE_NAME = process.env.NODECG_BUNDLE_NAME ?? "scoreko-dev";
|
||||
const DEFAULT_DASHBOARD_ROUTE = process.env.SCOREKO_DASHBOARD_ROUTE ?? "dashboard/index.html";
|
||||
const LOAD_DELAY_MS = Number.parseInt(process.env.ELECTRON_LOAD_DELAY_MS ?? "5000", 10);
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
||||
const nodecgPath = path.resolve(rootPath, "lib", "nodecg");
|
||||
const loadingPath = path.resolve(rootPath, "static", "loading.html");
|
||||
|
||||
const dashboardUrl = `http://localhost:${DEFAULT_NODECG_PORT}/bundles/${DEFAULT_BUNDLE_NAME}/${DEFAULT_DASHBOARD_ROUTE}`;
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let loadingWindow: BrowserWindow | null = null;
|
||||
let nodecgProcess: ChildProcess | null = null;
|
||||
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const win = new BrowserWindow({
|
||||
show: false,
|
||||
title: APP_TITLE,
|
||||
width: 1440,
|
||||
height: 900,
|
||||
minWidth: 960,
|
||||
minHeight: 640,
|
||||
backgroundColor: "#0f0f0f",
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url).catch((error) => {
|
||||
log("Error opening external url", url, error);
|
||||
});
|
||||
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
win.webContents.on("will-navigate", (event, url) => {
|
||||
if (url !== dashboardUrl) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url).catch((error) => {
|
||||
log("Error opening navigation url", url, error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
win.on("page-title-updated", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
function createLoadingWindow(): BrowserWindow {
|
||||
const win = new BrowserWindow({
|
||||
show: false,
|
||||
frame: false,
|
||||
title: APP_TITLE,
|
||||
width: 420,
|
||||
height: 280,
|
||||
resizable: false,
|
||||
movable: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
backgroundColor: "#0f0f0f",
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
win.on("page-title-updated", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
function startNodeCG(): ChildProcess {
|
||||
const indexPath = path.join(nodecgPath, "index.js");
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
throw new Error(`NodeCG entrypoint not found: ${indexPath}`);
|
||||
}
|
||||
|
||||
const child = fork(indexPath, {
|
||||
cwd: nodecgPath,
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
NODE_ENV: isDev ? "development" : "production",
|
||||
NODECG_PORT: DEFAULT_NODECG_PORT,
|
||||
},
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
log(`NodeCG started with pid=${child.pid}`);
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
|
||||
nodecgProcess = null;
|
||||
});
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
async function launch(): Promise<void> {
|
||||
mainWindow = createMainWindow();
|
||||
loadingWindow = createLoadingWindow();
|
||||
|
||||
await loadingWindow.loadFile(loadingPath);
|
||||
loadingWindow.show();
|
||||
|
||||
nodecgProcess = startNodeCG();
|
||||
|
||||
setTimeout(async () => {
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
await mainWindow.loadURL(dashboardUrl);
|
||||
mainWindow.show();
|
||||
|
||||
if (loadingWindow && !loadingWindow.isDestroyed()) {
|
||||
loadingWindow.close();
|
||||
loadingWindow = null;
|
||||
}
|
||||
}, LOAD_DELAY_MS);
|
||||
}
|
||||
|
||||
function stopNodeCG(): void {
|
||||
if (!nodecgProcess || nodecgProcess.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Stopping NodeCG pid=${nodecgProcess.pid}`);
|
||||
nodecgProcess.kill("SIGTERM");
|
||||
}
|
||||
|
||||
function log(...args: unknown[]): void {
|
||||
console.log("[scoreko-electron]", ...args);
|
||||
}
|
||||
|
||||
app.on("ready", () => {
|
||||
launch().catch((error: unknown) => {
|
||||
console.error("Failed to launch Scoreko wrapper", error);
|
||||
app.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
app.on("activate", async () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
mainWindow = createMainWindow();
|
||||
await mainWindow.loadURL(dashboardUrl);
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
stopNodeCG();
|
||||
});
|
||||
|
||||
process.on("exit", () => {
|
||||
stopNodeCG();
|
||||
});
|
||||
Reference in New Issue
Block a user