mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
Remove binary icon asset and align icon config/docs (#18)
This commit is contained in:
+118
-72
@@ -1,24 +1,53 @@
|
||||
import { app, BrowserWindow, shell } from "electron";
|
||||
import { app, BrowserWindow, BrowserWindowConstructorOptions, shell } from "electron";
|
||||
import { ChildProcess, spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const APP_TITLE = process.env.SCOREKO_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/example/main.html?standalone=true";
|
||||
const DEFAULT_LOADING_ROUTE = process.env.SCOREKO_LOADING_ROUTE ?? "dashboard/loading/main.html?standalone=true";
|
||||
const LOAD_DELAY_MS = parseEnvInt("ELECTRON_LOAD_DELAY_MS", 10000);
|
||||
const STARTUP_TIMEOUT_MS = parseEnvInt("NODECG_STARTUP_TIMEOUT_MS", 30000);
|
||||
const NODECG_KILL_TIMEOUT_MS = parseEnvInt("NODECG_KILL_TIMEOUT_MS", 2500);
|
||||
const NODECG_RUNTIME_NAME = "electron internal node";
|
||||
type AppRuntimeConfig = {
|
||||
title: string;
|
||||
userModelId: string;
|
||||
iconPathOverride?: string;
|
||||
nodecgPort: string;
|
||||
bundleName: string;
|
||||
dashboardRoute: string;
|
||||
loadingRoute: string;
|
||||
loadDelayMs: number;
|
||||
startupTimeoutMs: number;
|
||||
nodecgKillTimeoutMs: number;
|
||||
};
|
||||
|
||||
const RUNTIME_NAME = "electron internal node";
|
||||
const DEFAULT_WINDOW_BACKGROUND = "#0f0f0f";
|
||||
const DEFAULT_WINDOW_SIZE = {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 500,
|
||||
};
|
||||
const LOADING_WINDOW_SIZE = {
|
||||
width: 300,
|
||||
height: 300,
|
||||
};
|
||||
|
||||
const runtimeConfig: AppRuntimeConfig = {
|
||||
title: getEnv("SCOREKO_APP_TITLE", "Scoreko"),
|
||||
userModelId: getEnv("SCOREKO_APP_USER_MODEL_ID", "com.scoreko.desktop"),
|
||||
iconPathOverride: getOptionalEnv("SCOREKO_APP_ICON_PATH"),
|
||||
nodecgPort: getEnv("NODECG_PORT", "9090"),
|
||||
bundleName: getEnv("NODECG_BUNDLE_NAME", "scoreko-dev"),
|
||||
dashboardRoute: getEnv("SCOREKO_DASHBOARD_ROUTE", "dashboard/example/main.html?standalone=true"),
|
||||
loadingRoute: getEnv("SCOREKO_LOADING_ROUTE", "dashboard/loading/main.html?standalone=true"),
|
||||
loadDelayMs: parseEnvInt("ELECTRON_LOAD_DELAY_MS", 10000),
|
||||
startupTimeoutMs: parseEnvInt("NODECG_STARTUP_TIMEOUT_MS", 30000),
|
||||
nodecgKillTimeoutMs: parseEnvInt("NODECG_KILL_TIMEOUT_MS", 2500),
|
||||
};
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
||||
const nodecgPath = path.resolve(rootPath, "lib", "nodecg");
|
||||
const dashboardUrl = `http://localhost:${DEFAULT_NODECG_PORT}/bundles/${DEFAULT_BUNDLE_NAME}/${DEFAULT_DASHBOARD_ROUTE}`;
|
||||
const loadingUrl = `http://localhost:${DEFAULT_NODECG_PORT}/bundles/${DEFAULT_BUNDLE_NAME}/${DEFAULT_LOADING_ROUTE}`;
|
||||
const baseUrl = `http://127.0.0.1:${DEFAULT_NODECG_PORT}`;
|
||||
const dashboardUrl = `http://localhost:${runtimeConfig.nodecgPort}/bundles/${runtimeConfig.bundleName}/${runtimeConfig.dashboardRoute}`;
|
||||
const loadingUrl = `http://localhost:${runtimeConfig.nodecgPort}/bundles/${runtimeConfig.bundleName}/${runtimeConfig.loadingRoute}`;
|
||||
const baseUrl = `http://127.0.0.1:${runtimeConfig.nodecgPort}`;
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let loadingWindow: BrowserWindow | null = null;
|
||||
@@ -27,89 +56,93 @@ let stopNodeCGPromise: Promise<void> | null = null;
|
||||
let isQuitting = false;
|
||||
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const iconPath = resolveAppIconPath();
|
||||
const windowOptions = createWindowOptions({ isLoadingWindow: false });
|
||||
const window = new BrowserWindow(windowOptions);
|
||||
|
||||
const win = new BrowserWindow({
|
||||
show: false,
|
||||
title: APP_TITLE,
|
||||
...(iconPath ? { icon: iconPath } : {}),
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 500,
|
||||
backgroundColor: "#0f0f0f",
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
window.setMenuBarVisibility(false);
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
void shell.openExternal(url);
|
||||
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
win.webContents.on("will-navigate", (event, url) => {
|
||||
window.webContents.on("will-navigate", (event, url) => {
|
||||
if (url !== dashboardUrl) {
|
||||
event.preventDefault();
|
||||
void shell.openExternal(url);
|
||||
}
|
||||
});
|
||||
|
||||
win.on("page-title-updated", (event) => {
|
||||
window.on("page-title-updated", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
return win;
|
||||
return window;
|
||||
}
|
||||
|
||||
function createLoadingWindow(): BrowserWindow {
|
||||
const iconPath = resolveAppIconPath();
|
||||
const window = new BrowserWindow(createWindowOptions({ isLoadingWindow: true }));
|
||||
|
||||
const win = new BrowserWindow({
|
||||
show: false,
|
||||
frame: false,
|
||||
title: APP_TITLE,
|
||||
...(iconPath ? { icon: iconPath } : {}),
|
||||
width: 300,
|
||||
height: 300,
|
||||
resizable: false,
|
||||
movable: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
backgroundColor: "#0f0f0f",
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
win.on("page-title-updated", (event) => {
|
||||
window.on("page-title-updated", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
return win;
|
||||
return window;
|
||||
}
|
||||
|
||||
function createWindowOptions({ isLoadingWindow }: { isLoadingWindow: boolean }): BrowserWindowConstructorOptions {
|
||||
const iconPath = resolveAppIconPath();
|
||||
|
||||
const baseOptions: BrowserWindowConstructorOptions = {
|
||||
show: false,
|
||||
title: runtimeConfig.title,
|
||||
...(iconPath ? { icon: iconPath } : {}),
|
||||
backgroundColor: DEFAULT_WINDOW_BACKGROUND,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
...(isLoadingWindow ? {} : { nodeIntegration: false }),
|
||||
},
|
||||
};
|
||||
|
||||
if (isLoadingWindow) {
|
||||
return {
|
||||
...baseOptions,
|
||||
frame: false,
|
||||
width: LOADING_WINDOW_SIZE.width,
|
||||
height: LOADING_WINDOW_SIZE.height,
|
||||
resizable: false,
|
||||
movable: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseOptions,
|
||||
width: DEFAULT_WINDOW_SIZE.width,
|
||||
height: DEFAULT_WINDOW_SIZE.height,
|
||||
minWidth: DEFAULT_WINDOW_SIZE.minWidth,
|
||||
minHeight: DEFAULT_WINDOW_SIZE.minHeight,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAppIconPath(): string | undefined {
|
||||
const candidates = [
|
||||
const iconCandidates = [
|
||||
runtimeConfig.iconPathOverride,
|
||||
path.join(rootPath, "static", "icons", "icon.ico"),
|
||||
path.join(rootPath, "static", "icons", "icon.png"),
|
||||
path.join(rootPath, "static", "icon.ico"),
|
||||
path.join(rootPath, "static", "icon.png"),
|
||||
];
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
|
||||
return candidates.find((candidate) => fs.existsSync(candidate));
|
||||
return iconCandidates.find((candidate) => fs.existsSync(candidate));
|
||||
}
|
||||
|
||||
function validateNodeCGInstall(): void {
|
||||
const indexPath = path.join(nodecgPath, "index.js");
|
||||
const nodecgBootstrapPath = path.join(nodecgPath, "node_modules", "nodecg", "dist", "server", "bootstrap.js");
|
||||
const bundlePath = path.join(nodecgPath, "bundles", DEFAULT_BUNDLE_NAME);
|
||||
const bundlePath = path.join(nodecgPath, "bundles", runtimeConfig.bundleName);
|
||||
|
||||
if (!fs.existsSync(nodecgPath)) {
|
||||
throw new Error(`No existe la carpeta NodeCG: ${nodecgPath}`);
|
||||
@@ -133,7 +166,7 @@ function validateNodeCGInstall(): void {
|
||||
if (!fs.existsSync(bundlePath)) {
|
||||
throw new Error(
|
||||
[
|
||||
`No se encontró el bundle '${DEFAULT_BUNDLE_NAME}'.`,
|
||||
`No se encontró el bundle '${runtimeConfig.bundleName}'.`,
|
||||
`Ruta esperada: ${bundlePath}`,
|
||||
"Copia/clona tu bundle dentro de lib/nodecg/bundles antes de ejecutar Electron.",
|
||||
].join("\n"),
|
||||
@@ -150,7 +183,7 @@ function startNodeCG(): ChildProcess {
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: isDev ? "development" : "production",
|
||||
NODECG_PORT: DEFAULT_NODECG_PORT,
|
||||
NODECG_PORT: runtimeConfig.nodecgPort,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
@@ -159,16 +192,14 @@ function startNodeCG(): ChildProcess {
|
||||
});
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
const text = String(chunk);
|
||||
process.stdout.write(text);
|
||||
process.stdout.write(String(chunk));
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
const text = String(chunk);
|
||||
process.stderr.write(text);
|
||||
process.stderr.write(String(chunk));
|
||||
});
|
||||
|
||||
log(`NodeCG started with pid=${child.pid} using ${NODECG_RUNTIME_NAME}`);
|
||||
log(`NodeCG started with pid=${child.pid} using ${RUNTIME_NAME}`);
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
|
||||
@@ -179,7 +210,7 @@ function startNodeCG(): ChildProcess {
|
||||
}
|
||||
|
||||
async function waitForNodeCGReady(startTime: number): Promise<void> {
|
||||
while (Date.now() - startTime < STARTUP_TIMEOUT_MS) {
|
||||
while (Date.now() - startTime < runtimeConfig.startupTimeoutMs) {
|
||||
if (!nodecgProcess) {
|
||||
throw new Error("NodeCG terminó antes de estar listo.");
|
||||
}
|
||||
@@ -196,7 +227,7 @@ async function waitForNodeCGReady(startTime: number): Promise<void> {
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
throw new Error(`Timeout esperando NodeCG en ${baseUrl} (${STARTUP_TIMEOUT_MS}ms).`);
|
||||
throw new Error(`Timeout esperando NodeCG en ${baseUrl} (${runtimeConfig.startupTimeoutMs}ms).`);
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
@@ -228,7 +259,7 @@ async function launch(): Promise<void> {
|
||||
|
||||
await mainWindow.loadURL(dashboardUrl);
|
||||
|
||||
const remainingLoadingDelay = Math.max(0, LOAD_DELAY_MS - (Date.now() - loadingShownAt));
|
||||
const remainingLoadingDelay = Math.max(0, runtimeConfig.loadDelayMs - (Date.now() - loadingShownAt));
|
||||
if (remainingLoadingDelay > 0) {
|
||||
await sleep(remainingLoadingDelay);
|
||||
}
|
||||
@@ -303,7 +334,7 @@ function stopNodeCG(): Promise<void> {
|
||||
log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`);
|
||||
killNodeCGProcessTree(pid, "SIGKILL");
|
||||
}
|
||||
}, Math.max(0, NODECG_KILL_TIMEOUT_MS));
|
||||
}, Math.max(0, runtimeConfig.nodecgKillTimeoutMs));
|
||||
});
|
||||
|
||||
return stopNodeCGPromise;
|
||||
@@ -313,6 +344,15 @@ function log(...args: unknown[]): void {
|
||||
console.log("[scoreko-electron]", ...args);
|
||||
}
|
||||
|
||||
function getOptionalEnv(name: string): string | undefined {
|
||||
const value = process.env[name]?.trim();
|
||||
return value && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function getEnv(name: string, fallback: string): string {
|
||||
return getOptionalEnv(name) ?? fallback;
|
||||
}
|
||||
|
||||
function parseEnvInt(name: string, fallback: number): number {
|
||||
const rawValue = process.env[name];
|
||||
if (!rawValue) {
|
||||
@@ -333,6 +373,12 @@ function closeLoadingWindow(): void {
|
||||
}
|
||||
|
||||
app.on("ready", () => {
|
||||
app.setName(runtimeConfig.title);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId(runtimeConfig.userModelId);
|
||||
}
|
||||
|
||||
launch().catch(async (error: unknown) => {
|
||||
console.error("Failed to launch Scoreko wrapper", error);
|
||||
closeLoadingWindow();
|
||||
|
||||
Reference in New Issue
Block a user