mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-23 19:01:02 +00:00
feat: implement game launcher window functionality and enhance deep link handling
This commit is contained in:
@@ -819,6 +819,16 @@
|
||||
"learn_more": "Learn More",
|
||||
"debrid_description": "Download up to 4x faster with Nimbus"
|
||||
},
|
||||
"game_launcher": {
|
||||
"launching": "Launching...",
|
||||
"launching_base": "Launching",
|
||||
"open_hydra": "Open Hydra",
|
||||
"playtime": "Playtime",
|
||||
"amount_hours": "{{amount}} hours",
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"amount_hours_short": "{{amount}}h",
|
||||
"amount_minutes_short": "{{amount}}m"
|
||||
},
|
||||
"notifications_page": {
|
||||
"title": "Notifications",
|
||||
"mark_all_as_read": "Mark all as read",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { shell } from "electron";
|
||||
import { spawn } from "child_process";
|
||||
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";
|
||||
|
||||
const openGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -28,6 +29,9 @@ const openGame = async (
|
||||
launchOptions,
|
||||
});
|
||||
|
||||
// Always show the launcher window when launching a game
|
||||
WindowManager.createGameLauncherWindow(shop, objectId);
|
||||
|
||||
if (parsedParams.length === 0) {
|
||||
shell.openPath(parsedPath);
|
||||
return;
|
||||
|
||||
8
src/main/events/misc/close-game-launcher-window.ts
Normal file
8
src/main/events/misc/close-game-launcher-window.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const closeGameLauncherWindow = async () => {
|
||||
WindowManager.closeGameLauncherWindow();
|
||||
};
|
||||
|
||||
registerEvent("closeGameLauncherWindow", closeGameLauncherWindow);
|
||||
@@ -1,12 +1,15 @@
|
||||
import "./can-install-common-redist";
|
||||
import "./check-homebrew-folder-exists";
|
||||
import "./close-game-launcher-window";
|
||||
import "./delete-temp-file";
|
||||
import "./show-game-launcher-window";
|
||||
import "./get-hydra-decky-plugin-info";
|
||||
import "./hydra-api-call";
|
||||
import "./install-common-redist";
|
||||
import "./install-hydra-decky-plugin";
|
||||
import "./open-checkout";
|
||||
import "./open-external";
|
||||
import "./open-main-window";
|
||||
import "./save-temp-file";
|
||||
import "./show-item-in-folder";
|
||||
import "./show-open-dialog";
|
||||
|
||||
8
src/main/events/misc/open-main-window.ts
Normal file
8
src/main/events/misc/open-main-window.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const openMainWindow = async () => {
|
||||
WindowManager.openMainWindow();
|
||||
};
|
||||
|
||||
registerEvent("openMainWindow", openMainWindow);
|
||||
8
src/main/events/misc/show-game-launcher-window.ts
Normal file
8
src/main/events/misc/show-game-launcher-window.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const showGameLauncherWindow = async () => {
|
||||
WindowManager.showGameLauncherWindow();
|
||||
};
|
||||
|
||||
registerEvent("showGameLauncherWindow", showGameLauncherWindow);
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import resources from "@locales";
|
||||
import { PythonRPC } from "./services/python-rpc";
|
||||
import { db, gamesSublevel, levelKeys } from "./level";
|
||||
import { GameShop } from "@types";
|
||||
import { GameShop, UserPreferences } from "@types";
|
||||
import { parseExecutablePath } from "./events/helpers/parse-executable-path";
|
||||
import { parseLaunchOptions } from "./events/helpers/parse-launch-options";
|
||||
import { loadState } from "./main";
|
||||
@@ -144,16 +144,19 @@ app.whenReady().then(async () => {
|
||||
|
||||
if (language) i18n.changeLanguage(language);
|
||||
|
||||
if (!process.argv.includes("--hidden")) {
|
||||
// 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");
|
||||
|
||||
const deepLinkArg = process.argv.find((arg) =>
|
||||
arg.startsWith("hydralauncher://")
|
||||
);
|
||||
if (deepLinkArg) {
|
||||
handleDeepLinkPath(deepLinkArg);
|
||||
}
|
||||
@@ -172,6 +175,19 @@ const handleRunGame = async (shop: GameShop, objectId: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{ 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();
|
||||
}
|
||||
|
||||
const parsedPath = parseExecutablePath(game.executablePath);
|
||||
const parsedParams = parseLaunchOptions(game.launchOptions);
|
||||
|
||||
@@ -237,17 +253,23 @@ const handleDeepLinkPath = (uri?: string) => {
|
||||
};
|
||||
|
||||
app.on("second-instance", (_event, commandLine) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (WindowManager.mainWindow) {
|
||||
if (WindowManager.mainWindow.isMinimized())
|
||||
WindowManager.mainWindow.restore();
|
||||
const deepLink = commandLine.pop();
|
||||
|
||||
WindowManager.mainWindow.focus();
|
||||
} else {
|
||||
WindowManager.createMainWindow();
|
||||
// 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(commandLine.pop());
|
||||
handleDeepLinkPath(deepLink);
|
||||
});
|
||||
|
||||
app.on("open-url", (_event, url) => {
|
||||
|
||||
@@ -204,6 +204,9 @@ function onOpenGame(game: Game) {
|
||||
lastSyncTick: now,
|
||||
});
|
||||
|
||||
// Close the launcher window when game starts
|
||||
WindowManager.closeGameLauncherWindow();
|
||||
|
||||
// Hide Hydra to tray on game startup if enabled
|
||||
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
|
||||
@@ -30,6 +30,7 @@ import { logger } from "./logger";
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
public static notificationWindow: Electron.BrowserWindow | null = null;
|
||||
public static gameLauncherWindow: Electron.BrowserWindow | null = null;
|
||||
|
||||
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
||||
|
||||
@@ -516,6 +517,84 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly GAME_LAUNCHER_WINDOW_WIDTH = 550;
|
||||
private static readonly GAME_LAUNCHER_WINDOW_HEIGHT = 320;
|
||||
|
||||
public static async createGameLauncherWindow(shop: string, objectId: string) {
|
||||
if (this.gameLauncherWindow) {
|
||||
this.gameLauncherWindow.close();
|
||||
this.gameLauncherWindow = null;
|
||||
}
|
||||
|
||||
const display = screen.getPrimaryDisplay();
|
||||
const { width: displayWidth, height: displayHeight } = display.bounds;
|
||||
|
||||
const x = Math.round((displayWidth - this.GAME_LAUNCHER_WINDOW_WIDTH) / 2);
|
||||
const y = Math.round(
|
||||
(displayHeight - this.GAME_LAUNCHER_WINDOW_HEIGHT) / 2
|
||||
);
|
||||
|
||||
this.gameLauncherWindow = new BrowserWindow({
|
||||
width: this.GAME_LAUNCHER_WINDOW_WIDTH,
|
||||
height: this.GAME_LAUNCHER_WINDOW_HEIGHT,
|
||||
x,
|
||||
y,
|
||||
resizable: false,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
fullscreenable: false,
|
||||
frame: false,
|
||||
backgroundColor: "#1c1c1c",
|
||||
icon,
|
||||
skipTaskbar: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
|
||||
this.gameLauncherWindow.removeMenu();
|
||||
|
||||
this.loadWindowURL(
|
||||
this.gameLauncherWindow,
|
||||
`game-launcher?shop=${shop}&objectId=${objectId}`
|
||||
);
|
||||
|
||||
this.gameLauncherWindow.on("closed", () => {
|
||||
this.gameLauncherWindow = null;
|
||||
});
|
||||
|
||||
if (!app.isPackaged || isStaging) {
|
||||
this.gameLauncherWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
public static showGameLauncherWindow() {
|
||||
if (this.gameLauncherWindow && !this.gameLauncherWindow.isDestroyed()) {
|
||||
this.gameLauncherWindow.show();
|
||||
}
|
||||
}
|
||||
|
||||
public static closeGameLauncherWindow() {
|
||||
if (this.gameLauncherWindow) {
|
||||
this.gameLauncherWindow.close();
|
||||
this.gameLauncherWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static openMainWindow() {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.show();
|
||||
if (this.mainWindow.isMinimized()) {
|
||||
this.mainWindow.restore();
|
||||
}
|
||||
this.mainWindow.focus();
|
||||
} else {
|
||||
this.createMainWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public static redirect(hash: string) {
|
||||
if (!this.mainWindow) this.createMainWindow();
|
||||
this.loadMainWindowURL(hash);
|
||||
|
||||
@@ -675,6 +675,11 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
closeEditorWindow: (themeId?: string) =>
|
||||
ipcRenderer.invoke("closeEditorWindow", themeId),
|
||||
|
||||
/* Game Launcher Window */
|
||||
showGameLauncherWindow: () => ipcRenderer.invoke("showGameLauncherWindow"),
|
||||
closeGameLauncherWindow: () => ipcRenderer.invoke("closeGameLauncherWindow"),
|
||||
openMainWindow: () => ipcRenderer.invoke("openMainWindow"),
|
||||
|
||||
/* LevelDB Generic CRUD */
|
||||
leveldb: {
|
||||
get: (
|
||||
|
||||
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@@ -462,6 +462,11 @@ declare global {
|
||||
onCustomThemeUpdated: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
closeEditorWindow: (themeId?: string) => Promise<void>;
|
||||
|
||||
/* Game Launcher Window */
|
||||
showGameLauncherWindow: () => Promise<void>;
|
||||
closeGameLauncherWindow: () => Promise<void>;
|
||||
openMainWindow: () => Promise<void>;
|
||||
|
||||
/* Download Options */
|
||||
onNewDownloadOptions: (
|
||||
cb: (gamesWithNewOptions: { gameId: string; count: number }[]) => void
|
||||
|
||||
@@ -34,6 +34,7 @@ import ThemeEditor from "./pages/theme-editor/theme-editor";
|
||||
import Library from "./pages/library/library";
|
||||
import Notifications from "./pages/notifications/notifications";
|
||||
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
|
||||
import GameLauncher from "./pages/game-launcher/game-launcher";
|
||||
|
||||
console.log = logger.log;
|
||||
|
||||
@@ -98,6 +99,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
path="/achievement-notification"
|
||||
element={<AchievementNotification />}
|
||||
/>
|
||||
<Route path="/game-launcher" element={<GameLauncher />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
|
||||
180
src/renderer/src/pages/game-launcher/game-launcher.scss
Normal file
180
src/renderer/src/pages/game-launcher/game-launcher.scss
Normal file
@@ -0,0 +1,180 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.game-launcher {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: linear-gradient(135deg, #0d0d0d 0%, #1a1a2e 50%, #16213e 100%);
|
||||
-webkit-app-region: drag;
|
||||
padding: calc(globals.$spacing-unit * 3);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&__glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at top right,
|
||||
rgba(22, 177, 149, 0.15) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&__logo-badge {
|
||||
position: absolute;
|
||||
top: calc(globals.$spacing-unit * 2);
|
||||
right: calc(globals.$spacing-unit * 2);
|
||||
z-index: 3;
|
||||
|
||||
svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
fill: globals.$body-color;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
-webkit-app-region: no-drag;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__cover {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: 180px;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
background-color: globals.$dark-background-color;
|
||||
}
|
||||
|
||||
&__cover-placeholder {
|
||||
height: 100%;
|
||||
width: 180px;
|
||||
border-radius: 8px;
|
||||
background-color: globals.$dark-background-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: globals.$body-color;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 14px;
|
||||
color: globals.$muted-color;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__dots {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
animation: dots 1.5s steps(4, end) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0% {
|
||||
content: "";
|
||||
}
|
||||
25% {
|
||||
content: ".";
|
||||
}
|
||||
50% {
|
||||
content: "..";
|
||||
}
|
||||
75% {
|
||||
content: "...";
|
||||
}
|
||||
100% {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
&__stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding-top: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&__button {
|
||||
width: 100%;
|
||||
background-color: globals.$muted-color;
|
||||
border: solid 1px transparent;
|
||||
color: #0d0d0d;
|
||||
border-radius: 8px;
|
||||
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||
min-height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.2s;
|
||||
font-family: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: calc(globals.$spacing-unit);
|
||||
|
||||
&:hover {
|
||||
background-color: #dadbe1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: globals.$active-opacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/renderer/src/pages/game-launcher/game-launcher.tsx
Normal file
203
src/renderer/src/pages/game-launcher/game-launcher.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ImageIcon, ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import { darkenColor } from "@renderer/helpers";
|
||||
import { logger } from "@renderer/logger";
|
||||
import { average } from "color.js";
|
||||
import type { Game, GameShop, ShopAssets } from "@types";
|
||||
import "./game-launcher.scss";
|
||||
|
||||
export default function GameLauncher() {
|
||||
const { t } = useTranslation("game_launcher");
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const shop = searchParams.get("shop") as GameShop;
|
||||
const objectId = searchParams.get("objectId");
|
||||
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
const [gameAssets, setGameAssets] = useState<ShopAssets | null>(null);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [accentColor, setAccentColor] = useState<string | null>(null);
|
||||
const [colorExtracted, setColorExtracted] = useState(false);
|
||||
const [colorError, setColorError] = useState(false);
|
||||
const [windowShown, setWindowShown] = useState(false);
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
(playTimeInMilliseconds = 0) => {
|
||||
const minutes = playTimeInMilliseconds / 60000;
|
||||
|
||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||
return t("amount_minutes_short", { amount: minutes.toFixed(0) });
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours_short", { amount: hours.toFixed(1) });
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (shop && objectId) {
|
||||
window.electron.getGameByObjectId(shop, objectId).then((gameData) => {
|
||||
setGame(gameData);
|
||||
});
|
||||
|
||||
window.electron.getGameAssets(objectId, shop).then((assets) => {
|
||||
setGameAssets(assets);
|
||||
});
|
||||
}
|
||||
}, [shop, objectId]);
|
||||
|
||||
// Fallback auto-close timer - game detection will usually close it sooner
|
||||
useEffect(() => {
|
||||
if (!windowShown) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
window.electron.closeGameLauncherWindow();
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [windowShown]);
|
||||
|
||||
const handleOpenHydra = () => {
|
||||
window.electron.openMainWindow();
|
||||
window.electron.closeGameLauncherWindow();
|
||||
};
|
||||
|
||||
const coverImage =
|
||||
gameAssets?.coverImageUrl?.replaceAll("\\", "/") ||
|
||||
game?.iconUrl?.replaceAll("\\", "/") ||
|
||||
game?.libraryHeroImageUrl?.replaceAll("\\", "/") ||
|
||||
"";
|
||||
const gameTitle = game?.title ?? gameAssets?.title ?? "";
|
||||
const playTime = game?.playTimeInMilliseconds ?? 0;
|
||||
const achievementCount = game?.achievementCount ?? 0;
|
||||
const unlockedAchievements = game?.unlockedAchievementCount ?? 0;
|
||||
|
||||
const extractAccentColor = useCallback(async (imageUrl: string) => {
|
||||
try {
|
||||
const color = await average(imageUrl, { amount: 1, format: "hex" });
|
||||
const colorString = typeof color === "string" ? color : color.toString();
|
||||
setAccentColor(colorString);
|
||||
} catch (error) {
|
||||
logger.error("Failed to extract accent color:", error);
|
||||
setColorError(true);
|
||||
} finally {
|
||||
setColorExtracted(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Extract color as soon as we have the image URL
|
||||
useEffect(() => {
|
||||
if (coverImage && !colorExtracted) {
|
||||
extractAccentColor(coverImage);
|
||||
}
|
||||
}, [coverImage, colorExtracted, extractAccentColor]);
|
||||
|
||||
// Only show content when both image is loaded and color is extracted successfully
|
||||
const isReady = imageLoaded && colorExtracted && !colorError;
|
||||
const hasFailed =
|
||||
imageError || colorError || (!coverImage && gameAssets !== null);
|
||||
|
||||
// Show or close window based on loading state
|
||||
useEffect(() => {
|
||||
if (windowShown) return;
|
||||
|
||||
if (hasFailed) {
|
||||
window.electron.closeGameLauncherWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isReady) {
|
||||
window.electron.showGameLauncherWindow();
|
||||
setWindowShown(true);
|
||||
}
|
||||
}, [isReady, hasFailed, windowShown]);
|
||||
|
||||
const backgroundStyle = accentColor
|
||||
? {
|
||||
background: `linear-gradient(135deg, ${darkenColor(accentColor, 0.7)} 0%, ${darkenColor(accentColor, 0.8, 0.9)} 50%, ${darkenColor(accentColor, 0.85, 0.8)} 100%)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const glowStyle = accentColor
|
||||
? {
|
||||
background: `radial-gradient(ellipse at top right, ${darkenColor(accentColor, 0.3, 0.15)} 0%, transparent 50%)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="game-launcher" style={backgroundStyle}>
|
||||
<div className="game-launcher__glow" style={glowStyle} />
|
||||
|
||||
<div className="game-launcher__logo-badge">
|
||||
<HydraIcon />
|
||||
</div>
|
||||
|
||||
<div className="game-launcher__content">
|
||||
{imageError || !coverImage ? (
|
||||
<div className="game-launcher__cover-placeholder">
|
||||
<ImageIcon size={32} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!isReady && (
|
||||
<div className="game-launcher__cover-placeholder">
|
||||
<ImageIcon size={32} />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={coverImage}
|
||||
alt={gameTitle}
|
||||
className="game-launcher__cover"
|
||||
style={{ display: isReady ? "block" : "none" }}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="game-launcher__info">
|
||||
<div className="game-launcher__center">
|
||||
<h1 className="game-launcher__title">{gameTitle}</h1>
|
||||
|
||||
<p className="game-launcher__status">
|
||||
{t("launching_base")}
|
||||
<span className="game-launcher__dots" />
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="game-launcher__button"
|
||||
onClick={handleOpenHydra}
|
||||
>
|
||||
{t("open_hydra")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(playTime > 0 || achievementCount > 0) && (
|
||||
<div className="game-launcher__stats">
|
||||
{playTime > 0 && (
|
||||
<span className="game-launcher__stat">
|
||||
<ClockIcon size={14} />
|
||||
{formatPlayTime(playTime)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{achievementCount > 0 && (
|
||||
<span className="game-launcher__stat">
|
||||
<TrophyIcon size={14} />
|
||||
{unlockedAchievements}/{achievementCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user