mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-02 16:23:48 +03:00
fix: moving downloader directly to parser
This commit is contained in:
parent
4d32ff2ac2
commit
866ee7b30d
@ -18,7 +18,6 @@ import "./library/open-game";
|
|||||||
import "./library/open-game-installer";
|
import "./library/open-game-installer";
|
||||||
import "./library/remove-game";
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
import "./misc/get-or-cache-image";
|
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
|
@ -3,7 +3,7 @@ import { gameRepository } from "@main/repository";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { getImageBase64 } from "@main/helpers";
|
import { getFileBase64 } from "@main/helpers";
|
||||||
import { getSteamGameIconUrl } from "@main/services";
|
import { getSteamGameIconUrl } from "@main/services";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
@ -31,7 +31,7 @@ const addGameToLibrary = async (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID));
|
const iconUrl = await getFileBase64(await getSteamGameIconUrl(objectID));
|
||||||
|
|
||||||
return gameRepository.insert({
|
return gameRepository.insert({
|
||||||
title,
|
title,
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { getFileBuffer } from "@main/helpers";
|
|
||||||
import { logger } from "@main/services";
|
|
||||||
import { imageCachePath } from "@main/constants";
|
|
||||||
|
|
||||||
const getOrCacheImage = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
url: string
|
|
||||||
) => {
|
|
||||||
if (!fs.existsSync(imageCachePath)) fs.mkdirSync(imageCachePath);
|
|
||||||
|
|
||||||
const extname = path.extname(url);
|
|
||||||
|
|
||||||
const checksum = crypto.createHash("sha256").update(url).digest("hex");
|
|
||||||
const cachePath = path.join(imageCachePath, `${checksum}${extname}`);
|
|
||||||
|
|
||||||
const cache = fs.existsSync(cachePath);
|
|
||||||
|
|
||||||
if (cache) return `hydra://${cachePath}`;
|
|
||||||
|
|
||||||
getFileBuffer(url).then((buffer) =>
|
|
||||||
fs.writeFile(cachePath, buffer, (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error(`Failed to cache image`, err, {
|
|
||||||
method: "getOrCacheImage",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent(getOrCacheImage, {
|
|
||||||
name: "getOrCacheImage",
|
|
||||||
});
|
|
@ -5,7 +5,7 @@ import { GameStatus } from "@main/constants";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { getImageBase64 } from "@main/helpers";
|
import { getFileBase64 } from "@main/helpers";
|
||||||
import { In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
@ -72,7 +72,7 @@ const startGameDownload = async (
|
|||||||
|
|
||||||
return game;
|
return game;
|
||||||
} else {
|
} else {
|
||||||
const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID));
|
const iconUrl = await getFileBase64(await getSteamGameIconUrl(objectID));
|
||||||
|
|
||||||
const createdGame = await gameRepository.save({
|
const createdGame = await gameRepository.save({
|
||||||
title,
|
title,
|
||||||
|
@ -79,10 +79,24 @@ export const getFileBuffer = async (url: string) =>
|
|||||||
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getImageBase64 = async (url: string) =>
|
export const getFileBase64 = async (url: string) =>
|
||||||
getFileBuffer(url).then((buffer) => {
|
fetch(url, { method: "GET" }).then((response) =>
|
||||||
return `data:image/jpeg;base64,${Buffer.from(buffer).toString("base64")}`;
|
response.arrayBuffer().then((buffer) => {
|
||||||
});
|
const base64 = Buffer.from(buffer).toString("base64");
|
||||||
|
const contentType = response.headers.get("content-type");
|
||||||
|
|
||||||
|
return `data:${contentType};base64,${base64}`;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const steamUrlBuilder = {
|
||||||
|
library: (objectID: string) =>
|
||||||
|
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`,
|
||||||
|
libraryHero: (objectID: string) =>
|
||||||
|
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/library_hero.jpg`,
|
||||||
|
logo: (objectID: string) =>
|
||||||
|
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
|
||||||
|
};
|
||||||
|
|
||||||
export * from "./formatters";
|
export * from "./formatters";
|
||||||
export * from "./ps";
|
export * from "./ps";
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import cp from "node:child_process";
|
import cp from "node:child_process";
|
||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import { Notification, app, dialog } from "electron";
|
import { Notification, app, dialog } from "electron";
|
||||||
@ -99,25 +98,6 @@ export class TorrentClient {
|
|||||||
return game.progress;
|
return game.progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createTempIcon(encodedIcon: string): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const hash = crypto.randomBytes(16).toString("hex");
|
|
||||||
const iconPath = path.join(app.getPath("temp"), `${hash}.png`);
|
|
||||||
|
|
||||||
fs.writeFile(
|
|
||||||
iconPath,
|
|
||||||
Buffer.from(
|
|
||||||
encodedIcon.replace("data:image/jpeg;base64,", ""),
|
|
||||||
"base64"
|
|
||||||
),
|
|
||||||
(err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
resolve(iconPath);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async onSocketData(data: Buffer) {
|
public static async onSocketData(data: Buffer) {
|
||||||
const message = Buffer.from(data).toString("utf-8");
|
const message = Buffer.from(data).toString("utf-8");
|
||||||
|
|
||||||
@ -159,10 +139,7 @@ export class TorrentClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (userPreferences?.downloadNotificationsEnabled) {
|
if (userPreferences?.downloadNotificationsEnabled) {
|
||||||
const iconPath = await this.createTempIcon(game.iconUrl);
|
|
||||||
|
|
||||||
new Notification({
|
new Notification({
|
||||||
icon: iconPath,
|
|
||||||
title: t("download_complete", {
|
title: t("download_complete", {
|
||||||
ns: "notifications",
|
ns: "notifications",
|
||||||
lng: userPreferences.language,
|
lng: userPreferences.language,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow, Menu, Tray, app } from "electron";
|
import { BrowserWindow, Menu, Notification, Tray, app } from "electron";
|
||||||
import { is } from "@electron-toolkit/utils";
|
import { is } from "@electron-toolkit/utils";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
@ -54,6 +54,10 @@ export class WindowManager {
|
|||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mainWindow.on("ready-to-show", () => {
|
||||||
|
if (!app.isPackaged) WindowManager.mainWindow?.webContents.openDevTools();
|
||||||
|
});
|
||||||
|
|
||||||
this.mainWindow.on("close", () => {
|
this.mainWindow.on("close", () => {
|
||||||
if (userPreferences?.preferQuitInsteadOfHiding) {
|
if (userPreferences?.preferQuitInsteadOfHiding) {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { parentPort } from "worker_threads";
|
import { parentPort } from "worker_threads";
|
||||||
import parseTorrent from "parse-torrent";
|
import parseTorrent from "parse-torrent";
|
||||||
import { getFileBuffer } from "@main/helpers";
|
|
||||||
|
|
||||||
const port = parentPort;
|
const port = parentPort;
|
||||||
if (!port) throw new Error("IllegalState");
|
if (!port) throw new Error("IllegalState");
|
||||||
|
|
||||||
|
export const getFileBuffer = async (url: string) =>
|
||||||
|
fetch(url, { method: "GET" }).then((response) =>
|
||||||
|
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
||||||
|
);
|
||||||
|
|
||||||
port.on("message", async (url: string) => {
|
port.on("message", async (url: string) => {
|
||||||
const buffer = await getFileBuffer(url);
|
const buffer = await getFileBuffer(url);
|
||||||
const torrent = await parseTorrent(buffer);
|
const torrent = await parseTorrent(buffer);
|
||||||
|
|
||||||
port.postMessage(torrent);
|
port.postMessage(torrent);
|
||||||
});
|
});
|
||||||
|
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@ -94,7 +94,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getDiskFreeSpace: () => ipcRenderer.invoke("getDiskFreeSpace"),
|
getDiskFreeSpace: () => ipcRenderer.invoke("getDiskFreeSpace"),
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
getOrCacheImage: (url: string) => ipcRenderer.invoke("getOrCacheImage", url),
|
|
||||||
ping: () => ipcRenderer.invoke("ping"),
|
ping: () => ipcRenderer.invoke("ping"),
|
||||||
getVersion: () => ipcRenderer.invoke("getVersion"),
|
getVersion: () => ipcRenderer.invoke("getVersion"),
|
||||||
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
||||||
|
@ -104,7 +104,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("getDiskFreeSpace", path),
|
ipcRenderer.invoke("getDiskFreeSpace", path),
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
getOrCacheImage: (url: string) => ipcRenderer.invoke("getOrCacheImage", url),
|
|
||||||
ping: () => ipcRenderer.invoke("ping"),
|
ping: () => ipcRenderer.invoke("ping"),
|
||||||
getVersion: () => ipcRenderer.invoke("getVersion"),
|
getVersion: () => ipcRenderer.invoke("getVersion"),
|
||||||
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>Hydra</title>
|
<title>Hydra</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: hydra: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: #1c1c1c">
|
<body style="background-color: #1c1c1c">
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import { forwardRef, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export interface AsyncImageProps
|
|
||||||
extends React.DetailedHTMLProps<
|
|
||||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
|
||||||
HTMLImageElement
|
|
||||||
> {
|
|
||||||
onSettled?: (url: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AsyncImage = forwardRef<HTMLImageElement, AsyncImageProps>(
|
|
||||||
({ onSettled, ...props }, ref) => {
|
|
||||||
const [source, setSource] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.src && props.src.startsWith("http")) {
|
|
||||||
window.electron.getOrCacheImage(props.src).then((url) => {
|
|
||||||
setSource(url);
|
|
||||||
|
|
||||||
if (onSettled) onSettled(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [props.src, onSettled]);
|
|
||||||
|
|
||||||
return <img ref={ref} {...props} src={source ?? props.src} />;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
AsyncImage.displayName = "AsyncImage";
|
|
@ -4,8 +4,6 @@ import type { CatalogueEntry } from "@types";
|
|||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg?react";
|
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg?react";
|
||||||
|
|
||||||
import { AsyncImage } from "../async-image/async-image";
|
|
||||||
|
|
||||||
import * as styles from "./game-card.css";
|
import * as styles from "./game-card.css";
|
||||||
import { useAppSelector } from "@renderer/hooks";
|
import { useAppSelector } from "@renderer/hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -43,11 +41,7 @@ export function GameCard({ game, disabled, ...props }: GameCardProps) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className={styles.backdrop}>
|
||||||
<AsyncImage
|
<img src={game.cover} alt={game.title} className={styles.cover} />
|
||||||
src={game.cover}
|
|
||||||
alt={game.title}
|
|
||||||
className={styles.cover}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.titleContainer}>
|
<div className={styles.titleContainer}>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { AsyncImage } from "@renderer/components";
|
|
||||||
import * as styles from "./hero.css";
|
import * as styles from "./hero.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ShopDetails } from "@types";
|
import { ShopDetails } from "@types";
|
||||||
@ -35,14 +34,14 @@ export function Hero() {
|
|||||||
className={styles.hero}
|
className={styles.hero}
|
||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className={styles.backdrop}>
|
||||||
<AsyncImage
|
<img
|
||||||
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
|
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
|
||||||
alt={featuredGameDetails?.name}
|
alt={featuredGameDetails?.name}
|
||||||
className={styles.heroMedia}
|
className={styles.heroMedia}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<AsyncImage
|
<img
|
||||||
src={steamUrlBuilder.logo(FEATURED_GAME_ID)}
|
src={steamUrlBuilder.logo(FEATURED_GAME_ID)}
|
||||||
width="250px"
|
width="250px"
|
||||||
alt={featuredGameDetails?.name}
|
alt={featuredGameDetails?.name}
|
||||||
|
@ -5,6 +5,5 @@ export * from "./header/header";
|
|||||||
export * from "./hero/hero";
|
export * from "./hero/hero";
|
||||||
export * from "./modal/modal";
|
export * from "./modal/modal";
|
||||||
export * from "./sidebar/sidebar";
|
export * from "./sidebar/sidebar";
|
||||||
export * from "./async-image/async-image";
|
|
||||||
export * from "./text-field/text-field";
|
export * from "./text-field/text-field";
|
||||||
export * from "./checkbox-field/checkbox-field";
|
export * from "./checkbox-field/checkbox-field";
|
||||||
|
@ -74,7 +74,6 @@ export const menuItem = recipe({
|
|||||||
active: {
|
active: {
|
||||||
true: {
|
true: {
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
@ -97,11 +96,6 @@ export const menuItemButton = style({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: `9px ${SPACING_UNIT}px`,
|
padding: `9px ${SPACING_UNIT}px`,
|
||||||
selectors: {
|
|
||||||
[`${menuItem({ active: true }).split(" ")[1]} &`]: {
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const menuItemButtonLabel = style({
|
export const menuItemButtonLabel = style({
|
||||||
|
@ -4,7 +4,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import type { Game } from "@types";
|
import type { Game } from "@types";
|
||||||
|
|
||||||
import { AsyncImage, TextField } from "@renderer/components";
|
import { TextField } from "@renderer/components";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
|
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
@ -14,7 +14,6 @@ import DiscordLogo from "@renderer/assets/discord-icon.svg?react";
|
|||||||
import XLogo from "@renderer/assets/x-icon.svg?react";
|
import XLogo from "@renderer/assets/x-icon.svg?react";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar.css";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
@ -217,7 +216,11 @@ export function Sidebar() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AsyncImage className={styles.gameIcon} src={game.iconUrl} />
|
<img
|
||||||
|
className={styles.gameIcon}
|
||||||
|
src={game.iconUrl}
|
||||||
|
alt={game.title}
|
||||||
|
/>
|
||||||
<span className={styles.menuItemButtonLabel}>
|
<span className={styles.menuItemButtonLabel}>
|
||||||
{getGameTitle(game)}
|
{getGameTitle(game)}
|
||||||
</span>
|
</span>
|
||||||
|
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
@ -79,7 +79,6 @@ declare global {
|
|||||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
getOrCacheImage: (url: string) => Promise<string>;
|
|
||||||
openExternal: (src: string) => Promise<void>;
|
openExternal: (src: string) => Promise<void>;
|
||||||
getVersion: () => Promise<string>;
|
getVersion: () => Promise<string>;
|
||||||
ping: () => string;
|
ping: () => string;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { AsyncImage, Button, TextField } from "@renderer/components";
|
import { Button, TextField } from "@renderer/components";
|
||||||
import { formatDownloadProgress, steamUrlBuilder } from "@renderer/helpers";
|
import { formatDownloadProgress, steamUrlBuilder } from "@renderer/helpers";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import type { Game } from "@types";
|
import type { Game } from "@types";
|
||||||
@ -235,7 +235,7 @@ export function Downloads() {
|
|||||||
cancelled: game.status === "cancelled",
|
cancelled: game.status === "cancelled",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AsyncImage
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectID)}
|
src={steamUrlBuilder.library(game.objectID)}
|
||||||
className={styles.downloadCover}
|
className={styles.downloadCover}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
|
@ -12,7 +12,7 @@ import type {
|
|||||||
SteamAppDetails,
|
SteamAppDetails,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
import { AsyncImage, Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
|
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
|
||||||
import { useAppDispatch, useDownload } from "@renderer/hooks";
|
import { useAppDispatch, useDownload } from "@renderer/hooks";
|
||||||
@ -69,14 +69,16 @@ export function GameDetails() {
|
|||||||
|
|
||||||
const { game: gameDownloading, startDownload, isDownloading } = useDownload();
|
const { game: gameDownloading, startDownload, isDownloading } = useDownload();
|
||||||
|
|
||||||
const handleImageSettled = useCallback((url: string) => {
|
const heroImage = steamUrlBuilder.libraryHero(objectID!);
|
||||||
average(url, { amount: 1, format: "hex" })
|
|
||||||
|
const handleHeroLoad = () => {
|
||||||
|
average(heroImage, { amount: 1, format: "hex" })
|
||||||
.then((color) => {
|
.then((color) => {
|
||||||
const darkColor = new Color(color).darken(0.6).toString() as string;
|
const darkColor = new Color(color).darken(0.6).toString() as string;
|
||||||
setColor({ light: color as string, dark: darkColor });
|
setColor({ light: color as string, dark: darkColor });
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const getGame = useCallback(() => {
|
const getGame = useCallback(() => {
|
||||||
window.electron
|
window.electron
|
||||||
@ -218,15 +220,15 @@ export function GameDetails() {
|
|||||||
) : (
|
) : (
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
<div className={styles.hero}>
|
<div className={styles.hero}>
|
||||||
<AsyncImage
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(objectID!)}
|
src={heroImage}
|
||||||
className={styles.heroImage}
|
className={styles.heroImage}
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
onSettled={handleImageSettled}
|
onLoad={handleHeroLoad}
|
||||||
/>
|
/>
|
||||||
<div className={styles.heroBackdrop}>
|
<div className={styles.heroBackdrop}>
|
||||||
<div className={styles.heroContent}>
|
<div className={styles.heroContent}>
|
||||||
<AsyncImage
|
<img
|
||||||
src={steamUrlBuilder.logo(objectID!)}
|
src={steamUrlBuilder.logo(objectID!)}
|
||||||
style={{ width: 300, alignSelf: "flex-end" }}
|
style={{ width: 300, alignSelf: "flex-end" }}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user