import { app, BrowserWindow, net, protocol } from "electron"; import updater from "electron-updater"; import i18n from "i18next"; import path from "node:path"; import url from "node:url"; import { electronApp, optimizer } from "@electron-toolkit/utils"; import { logger, clearGamesPlaytime, WindowManager, Lock, Aria2, } from "@main/services"; import resources from "@locales"; import { PythonRPC } from "./services/python-rpc"; import { db, gamesSublevel, levelKeys } from "./level"; import { GameShop, UserPreferences } from "@types"; import { launchGame } from "./helpers"; import { loadState } from "./main"; const { autoUpdater } = updater; autoUpdater.setFeedURL({ provider: "github", owner: "hydralauncher", repo: "hydra", }); autoUpdater.logger = logger; const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) app.quit(); if (process.platform !== "linux") { app.commandLine.appendSwitch("--no-sandbox"); } i18n.init({ resources, lng: "en", fallbackLng: "en", interpolation: { escapeValue: false, }, }); const PROTOCOL = "hydralauncher"; if (process.defaultApp) { if (process.argv.length >= 2) { app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [ path.resolve(process.argv[1]), ]); } } else { app.setAsDefaultProtocolClient(PROTOCOL); } // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(async () => { electronApp.setAppUserModelId("gg.hydralauncher.hydra"); protocol.handle("local", (request) => { const filePath = request.url.slice("local:".length); return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString()); }); protocol.handle("gradient", (request) => { const gradientCss = decodeURIComponent( request.url.slice("gradient:".length) ); // Parse gradient CSS safely without regex to prevent ReDoS let direction = "45deg"; let color1 = "#4a90e2"; let color2 = "#7b68ee"; // Simple string parsing approach - more secure than regex if ( gradientCss.startsWith("linear-gradient(") && gradientCss.endsWith(")") ) { const content = gradientCss.slice(16, -1); // Remove "linear-gradient(" and ")" const parts = content.split(",").map((part) => part.trim()); if (parts.length >= 3) { direction = parts[0]; color1 = parts[1]; color2 = parts[2]; } } let x1 = "0%", y1 = "0%", x2 = "100%", y2 = "100%"; if (direction === "to right") { y2 = "0%"; } else if (direction === "to bottom") { x2 = "0%"; } else if (direction === "45deg") { y1 = "100%"; y2 = "0%"; } else if (direction === "225deg") { x1 = "100%"; x2 = "0%"; } else if (direction === "315deg") { x1 = "100%"; y1 = "100%"; x2 = "0%"; y2 = "0%"; } // Note: "135deg" case removed as it uses all default values const svgContent = ` `; return new Response(svgContent, { headers: { "Content-Type": "image/svg+xml" }, }); }); await loadState(); const language = await db .get(levelKeys.language, { valueEncoding: "utf8", }) .catch(() => "en"); if (language) i18n.changeLanguage(language); // Check if starting from a "run" deep link - don't show main window in that case const deepLinkArg = process.argv.find((arg) => arg.startsWith("hydralauncher://") ); const isRunDeepLink = deepLinkArg?.startsWith("hydralauncher://run"); if (!process.argv.includes("--hidden") && !isRunDeepLink) { WindowManager.createMainWindow(); } WindowManager.createNotificationWindow(); WindowManager.createSystemTray(language || "en"); if (deepLinkArg) { handleDeepLinkPath(deepLinkArg); } }); app.on("browser-window-created", (_, window) => { optimizer.watchWindowShortcuts(window); }); const handleRunGame = async (shop: GameShop, objectId: string) => { const gameKey = levelKeys.game(shop, objectId); const game = await gamesSublevel.get(gameKey); if (!game?.executablePath) { logger.error("Game not found or no executable path", { shop, objectId }); return; } const userPreferences = await db.get( levelKeys.userPreferences, { valueEncoding: "json" } ); // Only open main window if setting is disabled if (!userPreferences?.hideToTrayOnGameStart) { WindowManager.createMainWindow(); } await launchGame({ shop, objectId, executablePath: game.executablePath, launchOptions: game.launchOptions, }); }; const handleDeepLinkPath = (uri?: string) => { if (!uri) return; try { const url = new URL(uri); if (url.host === "run") { const shop = url.searchParams.get("shop") as GameShop | null; const objectId = url.searchParams.get("objectId"); if (shop && objectId) { handleRunGame(shop, objectId); } return; } if (url.host === "install-source") { WindowManager.redirect(`settings${url.search}`); return; } if (url.host === "profile") { const userId = url.searchParams.get("userId"); if (userId) { WindowManager.redirect(`profile/${userId}`); } return; } if (url.host === "install-theme") { const themeName = url.searchParams.get("theme"); const authorId = url.searchParams.get("authorId"); const authorName = url.searchParams.get("authorName"); if (themeName && authorId && authorName) { WindowManager.redirect( `settings?theme=${themeName}&authorId=${authorId}&authorName=${authorName}` ); } } } catch (error) { logger.error("Error handling deep link", uri, error); } }; app.on("second-instance", (_event, commandLine) => { const deepLink = commandLine.pop(); // Check if this is a "run" deep link - don't show main window in that case const isRunDeepLink = deepLink?.startsWith("hydralauncher://run"); if (!isRunDeepLink) { if (WindowManager.mainWindow) { if (WindowManager.mainWindow.isMinimized()) WindowManager.mainWindow.restore(); WindowManager.mainWindow.focus(); } else { WindowManager.createMainWindow(); } } handleDeepLinkPath(deepLink); }); app.on("open-url", (_event, url) => { handleDeepLinkPath(url); }); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on("window-all-closed", () => { WindowManager.mainWindow = null; }); let canAppBeClosed = false; app.on("before-quit", async (e) => { await Lock.releaseLock(); if (!canAppBeClosed) { e.preventDefault(); /* Disconnects libtorrent */ PythonRPC.kill(); Aria2.kill(); await clearGamesPlaytime(); canAppBeClosed = true; app.quit(); } }); app.on("activate", () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { WindowManager.createMainWindow(); } }); // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here.