mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-21 13:19:35 -03:00
Compare commits
1 Commits
main
...
feat/LBX-4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
335f4d33b9 |
@@ -382,9 +382,6 @@
|
||||
"audio": "Audio",
|
||||
"filter_by_source": "Filter by source",
|
||||
"no_repacks_found": "No sources found for this game",
|
||||
"source_online": "Source is online",
|
||||
"source_partial": "Some links are offline",
|
||||
"source_offline": "Source is offline",
|
||||
"delete_review": "Delete review",
|
||||
"remove_review": "Remove Review",
|
||||
"delete_review_modal_title": "Are you sure you want to delete your review?",
|
||||
|
||||
@@ -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<string | null> => {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,34 +40,6 @@
|
||||
gap: calc(globals.$spacing-unit * 1);
|
||||
color: globals.$body-color;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
padding-right: calc(globals.$spacing-unit * 4);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__availability-orb {
|
||||
position: absolute;
|
||||
top: calc(globals.$spacing-unit * 1.5);
|
||||
right: calc(globals.$spacing-unit * 1.5);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--online {
|
||||
background-color: #22c55e;
|
||||
box-shadow: 0 0 6px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
&--partial {
|
||||
background-color: #eab308;
|
||||
box-shadow: 0 0 6px rgba(234, 179, 8, 0.5);
|
||||
}
|
||||
|
||||
&--offline {
|
||||
background-color: #ef4444;
|
||||
opacity: 0.7;
|
||||
box-shadow: 0 0 6px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
&__repack-title {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
@@ -186,20 +185,6 @@ export function RepacksModal({
|
||||
);
|
||||
}, [repacks, hashesInDebrid]);
|
||||
|
||||
const getRepackAvailabilityStatus = (
|
||||
repack: GameRepack
|
||||
): "online" | "partial" | "offline" => {
|
||||
const unavailableSet = new Set(repack.unavailableUris ?? []);
|
||||
const availableCount = repack.uris.filter(
|
||||
(uri) => !unavailableSet.has(uri)
|
||||
).length;
|
||||
const unavailableCount = repack.uris.length - availableCount;
|
||||
|
||||
if (unavailableCount === 0) return "online";
|
||||
if (availableCount === 0) return "offline";
|
||||
return "partial";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const term = filterTerm.trim().toLowerCase();
|
||||
|
||||
@@ -378,8 +363,6 @@ export function RepacksModal({
|
||||
filteredRepacks.map((repack) => {
|
||||
const isLastDownloadedOption =
|
||||
checkIfLastDownloadedOption(repack);
|
||||
const availabilityStatus = getRepackAvailabilityStatus(repack);
|
||||
const tooltipId = `availability-orb-${repack.id}`;
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -388,13 +371,6 @@ export function RepacksModal({
|
||||
onClick={() => handleRepackClick(repack)}
|
||||
className="repacks-modal__repack-button"
|
||||
>
|
||||
<span
|
||||
className={`repacks-modal__availability-orb repacks-modal__availability-orb--${availabilityStatus}`}
|
||||
data-tooltip-id={tooltipId}
|
||||
data-tooltip-content={t(`source_${availabilityStatus}`)}
|
||||
/>
|
||||
<Tooltip id={tooltipId} />
|
||||
|
||||
<p className="repacks-modal__repack-title">
|
||||
{repack.title}
|
||||
{userPreferences?.enableNewDownloadOptionsBadges !==
|
||||
|
||||
Reference in New Issue
Block a user