From 335f4d33b92dbb49652d2b5a3f423208396dd3b7 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Wed, 21 Jan 2026 16:52:34 +0200 Subject: [PATCH] feat: implement deep link handling and game shortcut creation with icon download --- .../events/library/create-game-shortcut.ts | 110 ++++++++++++++---- src/main/index.ts | 51 +++++++- 2 files changed, 136 insertions(+), 25 deletions(-) diff --git a/src/main/events/library/create-game-shortcut.ts b/src/main/events/library/create-game-shortcut.ts index 5df10e97..c554ba49 100644 --- a/src/main/events/library/create-game-shortcut.ts +++ b/src/main/events/library/create-game-shortcut.ts @@ -1,12 +1,64 @@ import { registerEvent } from "../register-event"; import createDesktopShortcut from "create-desktop-shortcuts"; import path from "node:path"; +import fs from "node:fs"; import { app } from "electron"; +import axios from "axios"; import { removeSymbolsFromName } from "@shared"; import { GameShop, ShortcutLocation } from "@types"; import { gamesSublevel, levelKeys } from "@main/level"; import { SystemPath } from "@main/services/system-path"; -import { windowsStartMenuPath } from "@main/constants"; +import { ASSETS_PATH, windowsStartMenuPath } from "@main/constants"; +import { getGameAssets } from "../catalogue/get-game-assets"; +import { logger } from "@main/services"; + +const downloadIcon = async ( + shop: GameShop, + objectId: string, + iconUrl?: string | null +): Promise => { + const iconPath = path.join(ASSETS_PATH, `${shop}-${objectId}`, "icon.ico"); + + try { + if (fs.existsSync(iconPath)) { + return iconPath; + } + + if (!iconUrl) { + return null; + } + + fs.mkdirSync(path.dirname(iconPath), { recursive: true }); + + const response = await axios.get(iconUrl, { responseType: "arraybuffer" }); + fs.writeFileSync(iconPath, response.data); + + return iconPath; + } catch (error) { + logger.error("Failed to download game icon", error); + return null; + } +}; + +const createUrlShortcut = ( + shortcutPath: string, + url: string, + iconPath?: string | null +): boolean => { + try { + let content = `[InternetShortcut]\nURL=${url}\n`; + + if (iconPath) { + content += `IconFile=${iconPath}\nIconIndex=0\n`; + } + + fs.writeFileSync(shortcutPath, content); + return true; + } catch (error) { + logger.error("Failed to create URL shortcut", error); + return false; + } +}; const createGameShortcut = async ( _event: Electron.IpcMainInvokeEvent, @@ -17,30 +69,42 @@ const createGameShortcut = async ( const gameKey = levelKeys.game(shop, objectId); const game = await gamesSublevel.get(gameKey); - if (game) { - const filePath = game.executablePath; - - const windowVbsPath = app.isPackaged - ? path.join(process.resourcesPath, "windows.vbs") - : undefined; - - const options = { - filePath, - name: removeSymbolsFromName(game.title), - outputPath: - location === "desktop" - ? SystemPath.getPath("desktop") - : windowsStartMenuPath, - }; - - return createDesktopShortcut({ - windows: { ...options, VBScriptPath: windowVbsPath }, - linux: options, - osx: options, - }); + if (!game) { + return false; } - return false; + const shortcutName = removeSymbolsFromName(game.title); + const deepLink = `hydralauncher://run?shop=${shop}&objectId=${objectId}`; + const outputPath = + location === "desktop" + ? SystemPath.getPath("desktop") + : windowsStartMenuPath; + + const assets = shop === "custom" ? null : await getGameAssets(objectId, shop); + const iconPath = await downloadIcon(shop, objectId, assets?.iconUrl); + + if (process.platform === "win32") { + const shortcutPath = path.join(outputPath, `${shortcutName}.url`); + return createUrlShortcut(shortcutPath, deepLink, iconPath); + } + + const windowVbsPath = app.isPackaged + ? path.join(process.resourcesPath, "windows.vbs") + : undefined; + + const options = { + filePath: process.execPath, + arguments: deepLink, + name: shortcutName, + outputPath, + icon: iconPath ?? undefined, + }; + + return createDesktopShortcut({ + windows: { ...options, VBScriptPath: windowVbsPath }, + linux: options, + osx: options, + }); }; registerEvent("createGameShortcut", createGameShortcut); diff --git a/src/main/index.ts b/src/main/index.ts index 65e20144..e0415884 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,8 +1,9 @@ -import { app, BrowserWindow, net, protocol } from "electron"; +import { app, BrowserWindow, net, protocol, shell } 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, @@ -13,7 +14,10 @@ import { } from "@main/services"; import resources from "@locales"; import { PythonRPC } from "./services/python-rpc"; -import { db, levelKeys } from "./level"; +import { db, gamesSublevel, levelKeys } from "./level"; +import { GameShop } from "@types"; +import { parseExecutablePath } from "./events/helpers/parse-executable-path"; +import { parseLaunchOptions } from "./events/helpers/parse-launch-options"; import { loadState } from "./main"; const { autoUpdater } = updater; @@ -146,18 +150,61 @@ app.whenReady().then(async () => { WindowManager.createNotificationWindow(); WindowManager.createSystemTray(language || "en"); + + const deepLinkArg = process.argv.find((arg) => + arg.startsWith("hydralauncher://") + ); + 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 parsedPath = parseExecutablePath(game.executablePath); + const parsedParams = parseLaunchOptions(game.launchOptions); + + await gamesSublevel.put(gameKey, { + ...game, + executablePath: parsedPath, + }); + + if (parsedParams.length === 0) { + shell.openPath(parsedPath); + return; + } + + spawn(parsedPath, parsedParams, { shell: false, detached: true }); +}; + 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;