diff --git a/package.json b/package.json index 31784bd8..2393057b 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "lucide-react": "^0.544.0", "node-7z": "^3.0.0", "parse-torrent": "^11.0.18", + "png-to-ico": "^3.0.1", "rc-virtual-list": "^3.18.3", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", diff --git a/src/main/events/library/create-game-shortcut.ts b/src/main/events/library/create-game-shortcut.ts index c554ba49..29d052f0 100644 --- a/src/main/events/library/create-game-shortcut.ts +++ b/src/main/events/library/create-game-shortcut.ts @@ -4,6 +4,7 @@ import path from "node:path"; import fs from "node:fs"; import { app } from "electron"; import axios from "axios"; +import pngToIco from "png-to-ico"; import { removeSymbolsFromName } from "@shared"; import { GameShop, ShortcutLocation } from "@types"; import { gamesSublevel, levelKeys } from "@main/level"; @@ -17,25 +18,29 @@ const downloadIcon = async ( objectId: string, iconUrl?: string | null ): Promise => { - const iconPath = path.join(ASSETS_PATH, `${shop}-${objectId}`, "icon.ico"); + if (!iconUrl) { + return null; + } + + const iconDir = path.join(ASSETS_PATH, `${shop}-${objectId}`); + const iconPath = path.join(iconDir, "icon.ico"); try { if (fs.existsSync(iconPath)) { return iconPath; } - if (!iconUrl) { - return null; - } - - fs.mkdirSync(path.dirname(iconPath), { recursive: true }); + fs.mkdirSync(iconDir, { recursive: true }); const response = await axios.get(iconUrl, { responseType: "arraybuffer" }); - fs.writeFileSync(iconPath, response.data); + const imageBuffer = Buffer.from(response.data); + + const icoBuffer = await pngToIco(imageBuffer); + fs.writeFileSync(iconPath, icoBuffer); return iconPath; } catch (error) { - logger.error("Failed to download game icon", error); + logger.error("Failed to download/convert game icon", error); return null; } }; diff --git a/src/main/events/library/open-game.ts b/src/main/events/library/open-game.ts index d1d845f0..a225d472 100644 --- a/src/main/events/library/open-game.ts +++ b/src/main/events/library/open-game.ts @@ -1,11 +1,6 @@ import { registerEvent } from "../register-event"; -import { shell } from "electron"; -import { spawn } from "node:child_process"; -import { parseExecutablePath } from "../helpers/parse-executable-path"; -import { gamesSublevel, levelKeys } from "@main/level"; import { GameShop } from "@types"; -import { parseLaunchOptions } from "../helpers/parse-launch-options"; -import { WindowManager } from "@main/services"; +import { launchGame } from "@main/helpers"; const openGame = async ( _event: Electron.IpcMainInvokeEvent, @@ -14,32 +9,7 @@ const openGame = async ( executablePath: string, launchOptions?: string | null ) => { - const parsedPath = parseExecutablePath(executablePath); - const parsedParams = parseLaunchOptions(launchOptions); - - const gameKey = levelKeys.game(shop, objectId); - - const game = await gamesSublevel.get(gameKey); - - if (!game) return; - - await gamesSublevel.put(gameKey, { - ...game, - executablePath: parsedPath, - launchOptions, - }); - - // Always show the launcher window when launching a game - await WindowManager.createGameLauncherWindow(shop, objectId); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - - if (parsedParams.length === 0) { - shell.openPath(parsedPath); - return; - } - - spawn(parsedPath, parsedParams, { shell: false, detached: true }); + await launchGame({ shop, objectId, executablePath, launchOptions }); }; registerEvent("openGame", openGame); diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index 664dbd78..d77c4add 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -94,3 +94,4 @@ export const getThemeSoundPath = ( }; export * from "./reg-parser"; +export * from "./launch-game"; diff --git a/src/main/helpers/launch-game.ts b/src/main/helpers/launch-game.ts new file mode 100644 index 00000000..63a4f7e5 --- /dev/null +++ b/src/main/helpers/launch-game.ts @@ -0,0 +1,47 @@ +import { shell } from "electron"; +import { spawn } from "node:child_process"; +import { GameShop } from "@types"; +import { gamesSublevel, levelKeys } from "@main/level"; +import { WindowManager } from "@main/services"; +import { parseExecutablePath } from "../events/helpers/parse-executable-path"; +import { parseLaunchOptions } from "../events/helpers/parse-launch-options"; + +export interface LaunchGameOptions { + shop: GameShop; + objectId: string; + executablePath: string; + launchOptions?: string | null; +} + +/** + * Shows the launcher window and launches the game executable + * Shared between deep link handler and openGame event + */ +export const launchGame = async (options: LaunchGameOptions): Promise => { + const { shop, objectId, executablePath, launchOptions } = options; + + const parsedPath = parseExecutablePath(executablePath); + const parsedParams = parseLaunchOptions(launchOptions); + + const gameKey = levelKeys.game(shop, objectId); + const game = await gamesSublevel.get(gameKey); + + if (game) { + await gamesSublevel.put(gameKey, { + ...game, + executablePath: parsedPath, + launchOptions, + }); + } + + await WindowManager.createGameLauncherWindow(shop, objectId); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + if (parsedParams.length === 0) { + shell.openPath(parsedPath); + return; + } + + spawn(parsedPath, parsedParams, { shell: false, detached: true }); +}; diff --git a/src/main/index.ts b/src/main/index.ts index 61f8051c..9f4dee93 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,9 +1,8 @@ -import { app, BrowserWindow, net, protocol, shell } from "electron"; +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 { spawn } from "node:child_process"; import { electronApp, optimizer } from "@electron-toolkit/utils"; import { logger, @@ -16,8 +15,7 @@ import resources from "@locales"; import { PythonRPC } from "./services/python-rpc"; import { db, gamesSublevel, levelKeys } from "./level"; import { GameShop, UserPreferences } from "@types"; -import { parseExecutablePath } from "./events/helpers/parse-executable-path"; -import { parseLaunchOptions } from "./events/helpers/parse-launch-options"; +import { launchGame } from "./helpers"; import { loadState } from "./main"; const { autoUpdater } = updater; @@ -180,30 +178,17 @@ const handleRunGame = async (shop: GameShop, objectId: string) => { { valueEncoding: "json" } ); - // Always show the launcher window - await WindowManager.createGameLauncherWindow(shop, objectId); - // Only open main window if setting is disabled if (!userPreferences?.hideToTrayOnGameStart) { WindowManager.createMainWindow(); } - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const parsedPath = parseExecutablePath(game.executablePath); - const parsedParams = parseLaunchOptions(game.launchOptions); - - await gamesSublevel.put(gameKey, { - ...game, - executablePath: parsedPath, + await launchGame({ + shop, + objectId, + executablePath: game.executablePath, + launchOptions: game.launchOptions, }); - - if (parsedParams.length === 0) { - shell.openPath(parsedPath); - return; - } - - spawn(parsedPath, parsedParams, { shell: false, detached: true }); }; const handleDeepLinkPath = (uri?: string) => { diff --git a/yarn.lock b/yarn.lock index a247d7ea..78aa5623 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3383,6 +3383,13 @@ dependencies: undici-types "~6.21.0" +"@types/node@^22.10.3": + version "22.19.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.7.tgz#434094ee1731ae76c16083008590a5835a8c39c1" + integrity sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw== + dependencies: + undici-types "~6.21.0" + "@types/node@^22.7.7": version "22.18.12" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.18.12.tgz#e165d87bc25d7bf6d3657035c914db7485de84fb" @@ -7367,6 +7374,20 @@ plist@3.1.0, plist@^3.0.4, plist@^3.0.5, plist@^3.1.0: base64-js "^1.5.1" xmlbuilder "^15.1.1" +png-to-ico@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/png-to-ico/-/png-to-ico-3.0.1.tgz#6ad50bec9ffa40aa74265deadc5128fa4097dfbe" + integrity sha512-S8BOAoaGd9gT5uaemQ62arIY3Jzco7Uc7LwUTqRyqJDTsKqOAiyfyN4dSdT0D+Zf8XvgztgpRbM5wnQd7EgYwg== + dependencies: + "@types/node" "^22.10.3" + minimist "^1.2.8" + pngjs "^7.0.0" + +pngjs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" + integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== + possible-typed-array-names@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"