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:
@@ -9,7 +9,7 @@ Wrapper de Electron para empaquetar una instalación de NodeCG que incluya el bu
|
|||||||
## Qué hace
|
## Qué hace
|
||||||
|
|
||||||
- Arranca `lib/nodecg/index.js` como proceso hijo desde Electron.
|
- Arranca `lib/nodecg/index.js` como proceso hijo desde Electron.
|
||||||
- Muestra la ruta de dashboard de carga del bundle (`/bundles/<bundle>/dashboard/loading.html`) servida por NodeCG mientras inicia (no usa un archivo local del wrapper).
|
- Muestra la ruta de dashboard de carga del bundle (`/bundles/<bundle>/dashboard/loading.html`) servida por NodeCG mientras inicia.
|
||||||
- Carga el dashboard del bundle en `http://localhost:<puerto>/bundles/<bundle>/<ruta-dashboard>`.
|
- Carga el dashboard del bundle en `http://localhost:<puerto>/bundles/<bundle>/<ruta-dashboard>`.
|
||||||
- Empaqueta NodeCG + assets dentro de la app final con `electron-builder`.
|
- Empaqueta NodeCG + assets dentro de la app final con `electron-builder`.
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ scoreko-electron-dev/
|
|||||||
│ └─ bundles/
|
│ └─ bundles/
|
||||||
│ └─ scoreko-dev/
|
│ └─ scoreko-dev/
|
||||||
├─ src/main/main.ts
|
├─ src/main/main.ts
|
||||||
|
├─ static/icons/
|
||||||
└─ package.json
|
└─ package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -35,61 +36,74 @@ scoreko-electron-dev/
|
|||||||
- `npm run pack`: genera app sin instalador.
|
- `npm run pack`: genera app sin instalador.
|
||||||
- `npm run dist`: genera instalador.
|
- `npm run dist`: genera instalador.
|
||||||
- `npm run rebuild:native`: rebuild nativo auxiliar en `lib/nodecg`.
|
- `npm run rebuild:native`: rebuild nativo auxiliar en `lib/nodecg`.
|
||||||
|
|
||||||
## Variables de entorno útiles
|
## Variables de entorno útiles
|
||||||
|
|
||||||
- `NODECG_BUNDLE_NAME` (default: `scoreko-dev`)
|
- `NODECG_BUNDLE_NAME` (default: `scoreko-dev`)
|
||||||
|
- `NODECG_PORT` (default: `9090`)
|
||||||
- `SCOREKO_DASHBOARD_ROUTE` (default: `dashboard/example/main.html?standalone=true`)
|
- `SCOREKO_DASHBOARD_ROUTE` (default: `dashboard/example/main.html?standalone=true`)
|
||||||
- `SCOREKO_LOADING_ROUTE` (default: `dashboard/loading/main.html?standalone=true`)
|
- `SCOREKO_LOADING_ROUTE` (default: `dashboard/loading/main.html?standalone=true`)
|
||||||
- `SCOREKO_APP_TITLE` (default: `Scoreko`)
|
- `SCOREKO_APP_TITLE` (default: `Scoreko`)
|
||||||
|
- `SCOREKO_APP_USER_MODEL_ID` (default: `com.scoreko.desktop`)
|
||||||
|
- `SCOREKO_APP_ICON_PATH` (default: búsqueda automática en `static/icons/icon.ico` y `static/icons/icon.png`)
|
||||||
|
- `ELECTRON_LOAD_DELAY_MS` (default: `10000`)
|
||||||
|
- `NODECG_STARTUP_TIMEOUT_MS` (default: `30000`)
|
||||||
|
- `NODECG_KILL_TIMEOUT_MS` (default: `2500`)
|
||||||
|
|
||||||
|
## Assets de íconos incluidos
|
||||||
|
|
||||||
|
Se incluye `static/icons/` con placeholder editable:
|
||||||
|
|
||||||
|
- `static/icons/icon.svg`
|
||||||
|
|
||||||
|
> En este repo no se versionan binarios. Para distribución final agrega localmente `static/icons/icon.ico` (Windows) y/o `static/icons/icon.png` (Linux/macOS runtime).
|
||||||
|
|
||||||
## Personalización (Windows / Electron)
|
## Personalización (Windows / Electron)
|
||||||
|
|
||||||
### 1) Título de la app
|
### 1) Título de la app
|
||||||
|
|
||||||
- **Durante ejecución**: cambia `SCOREKO_APP_TITLE` para sobreescribir el título de ventana.
|
- **Runtime (ventanas):** `SCOREKO_APP_TITLE`.
|
||||||
- **En instalador y ejecutable**: cambia `build.productName` en `package.json`.
|
- **Build (instalador y ejecutable):** `build.productName`.
|
||||||
|
|
||||||
### 2) Ícono en barra de tareas y esquina superior de la ventana
|
### 2) Ícono en barra de tareas y esquina superior
|
||||||
|
|
||||||
Coloca tu icono en uno de estos paths (prioridad de arriba hacia abajo):
|
Electron toma automáticamente el primer archivo existente en este orden:
|
||||||
|
|
||||||
- `static/icons/icon.ico`
|
1. `SCOREKO_APP_ICON_PATH` (si lo defines)
|
||||||
- `static/icons/icon.png`
|
2. `static/icons/icon.ico`
|
||||||
- `static/icon.ico`
|
3. `static/icons/icon.png`
|
||||||
- `static/icon.png`
|
4. `static/icon.ico`
|
||||||
|
5. `static/icon.png`
|
||||||
|
|
||||||
Electron tomará automáticamente el primero que exista.
|
### 3) Ícono del `.exe` y accesos directos
|
||||||
|
|
||||||
### 3) Ícono del `.exe` (instalador/app empaquetada)
|
Quedó configurado en `package.json` con `build.win.icon` + `build.nsis.installerIcon`/`uninstallerIcon`.
|
||||||
|
|
||||||
En `package.json` dentro de `build.win` agrega:
|
### 4) Nombre/autor del popup de Firewall de Windows
|
||||||
|
|
||||||
```json
|
Ese diálogo usa metadata del ejecutable firmado:
|
||||||
"icon": "static/icons/icon.ico"
|
|
||||||
```
|
|
||||||
|
|
||||||
> Recomendado: `.ico` multi-resolución (16/24/32/48/64/128/256).
|
- Nombre de app: normalmente `build.productName` y metadata del binario.
|
||||||
|
- Publisher/autor: certificado de firma de código (si no firmas puede verse `Unknown publisher`).
|
||||||
|
|
||||||
### 4) Texto y autor que muestra el popup del Firewall de Windows
|
Campos clave a revisar:
|
||||||
|
|
||||||
Ese diálogo toma datos del ejecutable final:
|
- `description`
|
||||||
|
- `author`
|
||||||
|
- `build.productName`
|
||||||
|
- `build.win.executableName`
|
||||||
|
- `build.appId`
|
||||||
|
|
||||||
- **Nombre de app**: suele venir de `productName` y metadatos del ejecutable.
|
Además, firma el `.exe` con certificado (`CSC_LINK` / `CSC_KEY_PASSWORD` en `electron-builder`).
|
||||||
- **Publisher/Autor**: viene de la **firma de código** (certificado). Sin firma, suele salir `Unknown publisher`.
|
|
||||||
|
|
||||||
Para personalizarlo correctamente en builds de distribución:
|
## Checklist de personalización extra
|
||||||
|
|
||||||
1. Ajusta en `package.json`:
|
- `build.appId`
|
||||||
- `description`
|
- `build.artifactName`
|
||||||
- `author`
|
- `build.win.executableName`
|
||||||
- `build.productName`
|
- `build.win.icon`
|
||||||
2. Firma el `.exe` con un certificado de tu empresa/persona (`CSC_LINK`/`CSC_KEY_PASSWORD` en `electron-builder`).
|
- `build.nsis` (nombre del setup, íconos, shortcut)
|
||||||
|
- `build.mac.icon`
|
||||||
> Nota: en desarrollo (`npm run start` / `npm run dev`) puedes ver `GitHub, Inc.` porque estás ejecutando el binario de Electron oficial.
|
- `build.linux.icon`
|
||||||
|
- `SCOREKO_APP_USER_MODEL_ID`
|
||||||
### 5) Otros campos personalizables que te conviene revisar
|
- `SCOREKO_APP_ICON_PATH`
|
||||||
|
|
||||||
- `build.appId` (identificador único de la app).
|
|
||||||
- `build.win.executableName` (nombre del ejecutable sin extensión).
|
|
||||||
- `build.nsis` (nombre del instalador, iconos de instalador/desinstalador, etc.).
|
|
||||||
- `build.artifactName` (patrón de nombre del archivo de salida).
|
|
||||||
|
|||||||
+21
-3
@@ -2,6 +2,10 @@
|
|||||||
"name": "scoreko-electron-dev",
|
"name": "scoreko-electron-dev",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"description": "Electron wrapper to run NodeCG with the scoreko-dev bundle",
|
"description": "Electron wrapper to run NodeCG with the scoreko-dev bundle",
|
||||||
|
"author": {
|
||||||
|
"name": "Scoreko Team",
|
||||||
|
"email": "dev@scoreko.local"
|
||||||
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "dist/main/main.js",
|
"main": "dist/main/main.js",
|
||||||
@@ -21,6 +25,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"appId": "com.scoreko.desktop",
|
"appId": "com.scoreko.desktop",
|
||||||
"productName": "Scoreko",
|
"productName": "Scoreko",
|
||||||
|
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release",
|
"output": "release",
|
||||||
"buildResources": "static"
|
"buildResources": "static"
|
||||||
@@ -42,17 +47,30 @@
|
|||||||
"mac": {
|
"mac": {
|
||||||
"target": [
|
"target": [
|
||||||
"dmg"
|
"dmg"
|
||||||
]
|
],
|
||||||
|
"icon": "static/icons/icon.ico"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
"AppImage"
|
"AppImage"
|
||||||
]
|
],
|
||||||
|
"icon": "static/icons"
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
"nsis"
|
"nsis"
|
||||||
]
|
],
|
||||||
|
"icon": "static/icons/icon.ico",
|
||||||
|
"executableName": "Scoreko"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"artifactName": "${productName}-setup-${version}.${ext}",
|
||||||
|
"installerIcon": "static/icons/icon.ico",
|
||||||
|
"uninstallerIcon": "static/icons/icon.ico",
|
||||||
|
"installerHeaderIcon": "static/icons/icon.ico",
|
||||||
|
"shortcutName": "Scoreko"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+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 { ChildProcess, spawn } from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
const APP_TITLE = process.env.SCOREKO_APP_TITLE ?? "Scoreko";
|
type AppRuntimeConfig = {
|
||||||
const DEFAULT_NODECG_PORT = process.env.NODECG_PORT ?? "9090";
|
title: string;
|
||||||
const DEFAULT_BUNDLE_NAME = process.env.NODECG_BUNDLE_NAME ?? "scoreko-dev";
|
userModelId: string;
|
||||||
const DEFAULT_DASHBOARD_ROUTE = process.env.SCOREKO_DASHBOARD_ROUTE ?? "dashboard/example/main.html?standalone=true";
|
iconPathOverride?: string;
|
||||||
const DEFAULT_LOADING_ROUTE = process.env.SCOREKO_LOADING_ROUTE ?? "dashboard/loading/main.html?standalone=true";
|
nodecgPort: string;
|
||||||
const LOAD_DELAY_MS = parseEnvInt("ELECTRON_LOAD_DELAY_MS", 10000);
|
bundleName: string;
|
||||||
const STARTUP_TIMEOUT_MS = parseEnvInt("NODECG_STARTUP_TIMEOUT_MS", 30000);
|
dashboardRoute: string;
|
||||||
const NODECG_KILL_TIMEOUT_MS = parseEnvInt("NODECG_KILL_TIMEOUT_MS", 2500);
|
loadingRoute: string;
|
||||||
const NODECG_RUNTIME_NAME = "electron internal node";
|
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 isDev = !app.isPackaged;
|
||||||
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
||||||
const nodecgPath = path.resolve(rootPath, "lib", "nodecg");
|
const nodecgPath = path.resolve(rootPath, "lib", "nodecg");
|
||||||
const dashboardUrl = `http://localhost:${DEFAULT_NODECG_PORT}/bundles/${DEFAULT_BUNDLE_NAME}/${DEFAULT_DASHBOARD_ROUTE}`;
|
const dashboardUrl = `http://localhost:${runtimeConfig.nodecgPort}/bundles/${runtimeConfig.bundleName}/${runtimeConfig.dashboardRoute}`;
|
||||||
const loadingUrl = `http://localhost:${DEFAULT_NODECG_PORT}/bundles/${DEFAULT_BUNDLE_NAME}/${DEFAULT_LOADING_ROUTE}`;
|
const loadingUrl = `http://localhost:${runtimeConfig.nodecgPort}/bundles/${runtimeConfig.bundleName}/${runtimeConfig.loadingRoute}`;
|
||||||
const baseUrl = `http://127.0.0.1:${DEFAULT_NODECG_PORT}`;
|
const baseUrl = `http://127.0.0.1:${runtimeConfig.nodecgPort}`;
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let loadingWindow: BrowserWindow | null = null;
|
let loadingWindow: BrowserWindow | null = null;
|
||||||
@@ -27,89 +56,93 @@ let stopNodeCGPromise: Promise<void> | null = null;
|
|||||||
let isQuitting = false;
|
let isQuitting = false;
|
||||||
|
|
||||||
function createMainWindow(): BrowserWindow {
|
function createMainWindow(): BrowserWindow {
|
||||||
const iconPath = resolveAppIconPath();
|
const windowOptions = createWindowOptions({ isLoadingWindow: false });
|
||||||
|
const window = new BrowserWindow(windowOptions);
|
||||||
|
|
||||||
const win = new BrowserWindow({
|
window.setMenuBarVisibility(false);
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
win.setMenuBarVisibility(false);
|
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
||||||
void shell.openExternal(url);
|
void shell.openExternal(url);
|
||||||
|
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
|
|
||||||
win.webContents.on("will-navigate", (event, url) => {
|
window.webContents.on("will-navigate", (event, url) => {
|
||||||
if (url !== dashboardUrl) {
|
if (url !== dashboardUrl) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void shell.openExternal(url);
|
void shell.openExternal(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
win.on("page-title-updated", (event) => {
|
window.on("page-title-updated", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
return win;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoadingWindow(): BrowserWindow {
|
function createLoadingWindow(): BrowserWindow {
|
||||||
const iconPath = resolveAppIconPath();
|
const window = new BrowserWindow(createWindowOptions({ isLoadingWindow: true }));
|
||||||
|
|
||||||
const win = new BrowserWindow({
|
window.on("page-title-updated", (event) => {
|
||||||
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) => {
|
|
||||||
event.preventDefault();
|
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 {
|
function resolveAppIconPath(): string | undefined {
|
||||||
const candidates = [
|
const iconCandidates = [
|
||||||
|
runtimeConfig.iconPathOverride,
|
||||||
path.join(rootPath, "static", "icons", "icon.ico"),
|
path.join(rootPath, "static", "icons", "icon.ico"),
|
||||||
path.join(rootPath, "static", "icons", "icon.png"),
|
path.join(rootPath, "static", "icons", "icon.png"),
|
||||||
path.join(rootPath, "static", "icon.ico"),
|
path.join(rootPath, "static", "icon.ico"),
|
||||||
path.join(rootPath, "static", "icon.png"),
|
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 {
|
function validateNodeCGInstall(): void {
|
||||||
const indexPath = path.join(nodecgPath, "index.js");
|
const indexPath = path.join(nodecgPath, "index.js");
|
||||||
const nodecgBootstrapPath = path.join(nodecgPath, "node_modules", "nodecg", "dist", "server", "bootstrap.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)) {
|
if (!fs.existsSync(nodecgPath)) {
|
||||||
throw new Error(`No existe la carpeta NodeCG: ${nodecgPath}`);
|
throw new Error(`No existe la carpeta NodeCG: ${nodecgPath}`);
|
||||||
@@ -133,7 +166,7 @@ function validateNodeCGInstall(): void {
|
|||||||
if (!fs.existsSync(bundlePath)) {
|
if (!fs.existsSync(bundlePath)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
[
|
[
|
||||||
`No se encontró el bundle '${DEFAULT_BUNDLE_NAME}'.`,
|
`No se encontró el bundle '${runtimeConfig.bundleName}'.`,
|
||||||
`Ruta esperada: ${bundlePath}`,
|
`Ruta esperada: ${bundlePath}`,
|
||||||
"Copia/clona tu bundle dentro de lib/nodecg/bundles antes de ejecutar Electron.",
|
"Copia/clona tu bundle dentro de lib/nodecg/bundles antes de ejecutar Electron.",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
@@ -150,7 +183,7 @@ function startNodeCG(): ChildProcess {
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
NODE_ENV: isDev ? "development" : "production",
|
NODE_ENV: isDev ? "development" : "production",
|
||||||
NODECG_PORT: DEFAULT_NODECG_PORT,
|
NODECG_PORT: runtimeConfig.nodecgPort,
|
||||||
ELECTRON_RUN_AS_NODE: "1",
|
ELECTRON_RUN_AS_NODE: "1",
|
||||||
},
|
},
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
@@ -159,16 +192,14 @@ function startNodeCG(): ChildProcess {
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.stdout?.on("data", (chunk) => {
|
child.stdout?.on("data", (chunk) => {
|
||||||
const text = String(chunk);
|
process.stdout.write(String(chunk));
|
||||||
process.stdout.write(text);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr?.on("data", (chunk) => {
|
child.stderr?.on("data", (chunk) => {
|
||||||
const text = String(chunk);
|
process.stderr.write(String(chunk));
|
||||||
process.stderr.write(text);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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) => {
|
child.on("exit", (code, signal) => {
|
||||||
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
|
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
|
||||||
@@ -179,7 +210,7 @@ function startNodeCG(): ChildProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function waitForNodeCGReady(startTime: number): Promise<void> {
|
async function waitForNodeCGReady(startTime: number): Promise<void> {
|
||||||
while (Date.now() - startTime < STARTUP_TIMEOUT_MS) {
|
while (Date.now() - startTime < runtimeConfig.startupTimeoutMs) {
|
||||||
if (!nodecgProcess) {
|
if (!nodecgProcess) {
|
||||||
throw new Error("NodeCG terminó antes de estar listo.");
|
throw new Error("NodeCG terminó antes de estar listo.");
|
||||||
}
|
}
|
||||||
@@ -196,7 +227,7 @@ async function waitForNodeCGReady(startTime: number): Promise<void> {
|
|||||||
await sleep(500);
|
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> {
|
function sleep(ms: number): Promise<void> {
|
||||||
@@ -228,7 +259,7 @@ async function launch(): Promise<void> {
|
|||||||
|
|
||||||
await mainWindow.loadURL(dashboardUrl);
|
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) {
|
if (remainingLoadingDelay > 0) {
|
||||||
await sleep(remainingLoadingDelay);
|
await sleep(remainingLoadingDelay);
|
||||||
}
|
}
|
||||||
@@ -303,7 +334,7 @@ function stopNodeCG(): Promise<void> {
|
|||||||
log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`);
|
log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`);
|
||||||
killNodeCGProcessTree(pid, "SIGKILL");
|
killNodeCGProcessTree(pid, "SIGKILL");
|
||||||
}
|
}
|
||||||
}, Math.max(0, NODECG_KILL_TIMEOUT_MS));
|
}, Math.max(0, runtimeConfig.nodecgKillTimeoutMs));
|
||||||
});
|
});
|
||||||
|
|
||||||
return stopNodeCGPromise;
|
return stopNodeCGPromise;
|
||||||
@@ -313,6 +344,15 @@ function log(...args: unknown[]): void {
|
|||||||
console.log("[scoreko-electron]", ...args);
|
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 {
|
function parseEnvInt(name: string, fallback: number): number {
|
||||||
const rawValue = process.env[name];
|
const rawValue = process.env[name];
|
||||||
if (!rawValue) {
|
if (!rawValue) {
|
||||||
@@ -333,6 +373,12 @@ function closeLoadingWindow(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.on("ready", () => {
|
app.on("ready", () => {
|
||||||
|
app.setName(runtimeConfig.title);
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
app.setAppUserModelId(runtimeConfig.userModelId);
|
||||||
|
}
|
||||||
|
|
||||||
launch().catch(async (error: unknown) => {
|
launch().catch(async (error: unknown) => {
|
||||||
console.error("Failed to launch Scoreko wrapper", error);
|
console.error("Failed to launch Scoreko wrapper", error);
|
||||||
closeLoadingWindow();
|
closeLoadingWindow();
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="Generic app icon">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0ea5e9"/>
|
||||||
|
<stop offset="100%" stop-color="#2563eb"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="32" y="32" width="448" height="448" rx="96" fill="url(#bg)"/>
|
||||||
|
<circle cx="256" cy="214" r="78" fill="#ffffff" fill-opacity="0.92"/>
|
||||||
|
<rect x="156" y="316" width="200" height="36" rx="18" fill="#ffffff" fill-opacity="0.92"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 568 B |
Reference in New Issue
Block a user