mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-05 21:22:07 +00:00
41e4e91c4b
- Update .gitignore and .prettierignore to exclude additional cache and configuration files. - Revise README.md for clarity on build processes and runtime behavior. - Improve architecture documentation to reflect changes in startup flow and module responsibilities. - Modify troubleshooting guide to address common runtime issues and installation steps. - Enhance ESLint configuration to ignore more directories. - Update package.json scripts for better build and distribution processes. - Introduce build-scoreko-bundle.mjs for building the Scoreko bundle. - Implement prepare-nodecg-runtime.mjs for managing NodeCG runtime installation and updates. - Add runtime-provisioner.ts to handle user-specific NodeCG runtime provisioning. - Create tests for runtime provisioning to ensure correct behavior. - Refactor process-manager.ts and main.ts to integrate new runtime management logic.
199 lines
5.3 KiB
TypeScript
199 lines
5.3 KiB
TypeScript
import { app, BrowserWindow } from "electron";
|
|
import path from "node:path";
|
|
|
|
import { getRuntimeConfig } from "./config/runtime-config";
|
|
import { showFatalError, log } from "./errors/error-presenter";
|
|
import { createNodecgProcessManager, NodecgProcessManager } from "./nodecg/process-manager";
|
|
import { prepareUserNodecgRuntime } from "./nodecg/runtime-provisioner";
|
|
import { getRemainingDelayMs } from "./utils/timing";
|
|
import { createLoadingWindow, createMainWindow } from "./windows/window-factory";
|
|
|
|
const appConfig = getRuntimeConfig();
|
|
|
|
// Force a stable userData folder name; overridable via SCOREKO_APP_USER_DATA_DIRECTORY.
|
|
app.setName(appConfig.title);
|
|
app.setPath("userData", path.join(app.getPath("appData"), appConfig.userDataDirectoryName));
|
|
|
|
const isDev = !app.isPackaged;
|
|
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
|
const sourceNodecgRuntimePath = path.resolve(rootPath, "lib", "nodecg");
|
|
const mainDashboardUrl = `http://localhost:${appConfig.nodecgPort}/bundles/${appConfig.bundleName}/${appConfig.mainDashboardRoute}`;
|
|
const loadingDashboardUrl = `http://localhost:${appConfig.nodecgPort}/bundles/${appConfig.bundleName}/${appConfig.loadingDashboardRoute}`;
|
|
const nodecgBaseUrl = `http://127.0.0.1:${appConfig.nodecgPort}`;
|
|
|
|
const hasSingleInstanceLock = app.requestSingleInstanceLock();
|
|
|
|
if (!hasSingleInstanceLock) {
|
|
app.quit();
|
|
}
|
|
|
|
type AppShutdownState = "running" | "stopping" | "stopped";
|
|
|
|
let mainWindow: BrowserWindow | null = null;
|
|
let loadingWindow: BrowserWindow | null = null;
|
|
let nodecgManager: NodecgProcessManager | null = null;
|
|
let shutdownState: AppShutdownState = "running";
|
|
|
|
function focusExistingWindow(): void {
|
|
const targetWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : loadingWindow;
|
|
|
|
if (!targetWindow || targetWindow.isDestroyed()) {
|
|
return;
|
|
}
|
|
|
|
if (targetWindow.isMinimized()) {
|
|
targetWindow.restore();
|
|
}
|
|
|
|
targetWindow.show();
|
|
targetWindow.focus();
|
|
}
|
|
|
|
async function launchApplication(): Promise<void> {
|
|
const nodecgRootPath = prepareUserNodecgRuntime({
|
|
sourceRuntimePath: sourceNodecgRuntimePath,
|
|
userDataPath: app.getPath("userData"),
|
|
appVersion: app.getVersion(),
|
|
bundleName: appConfig.bundleName,
|
|
log,
|
|
});
|
|
|
|
nodecgManager = createNodecgProcessManager({
|
|
isDev,
|
|
nodecgRootPath,
|
|
nodecgBaseUrl,
|
|
appConfig,
|
|
log,
|
|
});
|
|
|
|
// We create both windows early so startup feels instant while NodeCG is booting in the background.
|
|
mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl });
|
|
loadingWindow = createLoadingWindow({ appConfig, rootPath });
|
|
|
|
await nodecgManager.startNodecgProcess();
|
|
|
|
await nodecgManager.waitForNodecgReady(Date.now());
|
|
|
|
if (!loadingWindow || loadingWindow.isDestroyed()) {
|
|
return;
|
|
}
|
|
|
|
await loadingWindow.loadURL(loadingDashboardUrl);
|
|
loadingWindow.show();
|
|
|
|
const loadingShownAt = Date.now();
|
|
|
|
if (!mainWindow) {
|
|
return;
|
|
}
|
|
|
|
await mainWindow.loadURL(mainDashboardUrl);
|
|
|
|
// Keep the loading overlay visible for a minimum amount of time to avoid abrupt flashes.
|
|
const remainingLoadingDelay = getRemainingDelayMs(appConfig.loadDelayMs, loadingShownAt);
|
|
if (remainingLoadingDelay > 0) {
|
|
await sleep(remainingLoadingDelay);
|
|
}
|
|
|
|
mainWindow.show();
|
|
closeLoadingWindow();
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
function closeLoadingWindow(): void {
|
|
if (!loadingWindow || loadingWindow.isDestroyed()) {
|
|
return;
|
|
}
|
|
|
|
loadingWindow.close();
|
|
loadingWindow = null;
|
|
}
|
|
|
|
function stopNodecgGracefully(): Promise<void> {
|
|
if (shutdownState === "stopped") {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (shutdownState === "stopping") {
|
|
return nodecgManager?.stopNodecgProcessGracefully() ?? Promise.resolve();
|
|
}
|
|
|
|
shutdownState = "stopping";
|
|
|
|
return (nodecgManager?.stopNodecgProcessGracefully() ?? Promise.resolve()).finally(() => {
|
|
shutdownState = "stopped";
|
|
});
|
|
}
|
|
|
|
app.on("ready", () => {
|
|
if (!hasSingleInstanceLock) {
|
|
return;
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
app.setAppUserModelId(appConfig.userModelId);
|
|
}
|
|
|
|
launchApplication().catch((error: unknown) => {
|
|
showFatalError("No se pudo iniciar Scoreko.", error);
|
|
closeLoadingWindow();
|
|
app.exit(1);
|
|
});
|
|
});
|
|
|
|
app.on("second-instance", () => {
|
|
focusExistingWindow();
|
|
});
|
|
|
|
app.on("activate", async () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl });
|
|
await mainWindow.loadURL(mainDashboardUrl);
|
|
mainWindow.show();
|
|
}
|
|
});
|
|
|
|
app.on("window-all-closed", () => {
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on("before-quit", (event) => {
|
|
if (shutdownState !== "running") {
|
|
return;
|
|
}
|
|
|
|
// Block the default quit flow until we ask NodeCG to stop cleanly.
|
|
event.preventDefault();
|
|
|
|
stopNodecgGracefully().finally(() => {
|
|
app.quit();
|
|
});
|
|
});
|
|
|
|
app.on("will-quit", () => {
|
|
if (shutdownState === "running") {
|
|
void stopNodecgGracefully();
|
|
}
|
|
});
|
|
|
|
process.on("exit", () => {
|
|
if (shutdownState === "running") {
|
|
void stopNodecgGracefully();
|
|
}
|
|
});
|
|
|
|
process.on("uncaughtException", (error) => {
|
|
showFatalError("Unexpected error in Electron main process.", error);
|
|
});
|
|
|
|
process.on("unhandledRejection", (reason) => {
|
|
showFatalError("Unhandled promise in Electron main process.", reason);
|
|
});
|