Merge branch 'main' into main

This commit is contained in:
Hydra 2024-05-07 05:55:35 +01:00 committed by GitHub
commit 97ef496703
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 858 additions and 219 deletions

View file

@ -6,7 +6,7 @@
<title>Hydra</title>
<meta
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; media-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; media-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
/>
</head>
<body style="background-color: #1c1c1c">

View file

@ -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";

View file

@ -4,8 +4,6 @@ import type { CatalogueEntry } from "@types";
import SteamLogo from "@renderer/assets/steam-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 { useAppSelector } from "@renderer/hooks";
import { useTranslation } from "react-i18next";
@ -43,11 +41,7 @@ export function GameCard({ game, disabled, ...props }: GameCardProps) {
disabled={disabled}
>
<div className={styles.backdrop}>
<AsyncImage
src={game.cover}
alt={game.title}
className={styles.cover}
/>
<img src={game.cover} alt={game.title} className={styles.cover} />
<div className={styles.content}>
<div className={styles.titleContainer}>

View file

@ -1,5 +1,4 @@
import { useNavigate } from "react-router-dom";
import { AsyncImage } from "@renderer/components";
import * as styles from "./hero.css";
import { useEffect, useState } from "react";
import { ShopDetails } from "@types";
@ -35,14 +34,14 @@ export function Hero() {
className={styles.hero}
>
<div className={styles.backdrop}>
<AsyncImage
<img
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
alt={featuredGameDetails?.name}
className={styles.heroMedia}
/>
<div className={styles.content}>
<AsyncImage
<img
src={steamUrlBuilder.logo(FEATURED_GAME_ID)}
width="250px"
alt={featuredGameDetails?.name}

View file

@ -5,6 +5,5 @@ export * from "./header/header";
export * from "./hero/hero";
export * from "./modal/modal";
export * from "./sidebar/sidebar";
export * from "./async-image/async-image";
export * from "./text-field/text-field";
export * from "./checkbox-field/checkbox-field";

View file

@ -74,7 +74,6 @@ export const menuItem = recipe({
active: {
true: {
backgroundColor: "rgba(255, 255, 255, 0.1)",
fontWeight: "bold",
},
},
muted: {
@ -97,11 +96,6 @@ export const menuItemButton = style({
overflow: "hidden",
width: "100%",
padding: `9px ${SPACING_UNIT}px`,
selectors: {
[`${menuItem({ active: true }).split(" ")[1]} &`]: {
fontWeight: "bold",
},
},
});
export const menuItemButtonLabel = style({

View file

@ -4,7 +4,7 @@ import { useLocation, useNavigate } from "react-router-dom";
import type { Game } from "@types";
import { AsyncImage, TextField } from "@renderer/components";
import { TextField } from "@renderer/components";
import { useDownload, useLibrary } from "@renderer/hooks";
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 * as styles from "./sidebar.css";
import { vars } from "@renderer/theme.css";
const SIDEBAR_MIN_WIDTH = 200;
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}>
{getGameTitle(game)}
</span>

View file

@ -56,7 +56,7 @@ declare global {
objectID: string,
title: string,
shop: GameShop,
executablePath: string
executablePath: string | null
) => Promise<void>;
getLibrary: () => Promise<Game[]>;
getRepackersFriendlyNames: () => Promise<Record<string, string>>;
@ -74,12 +74,12 @@ declare global {
updateUserPreferences: (
preferences: Partial<UserPreferences>
) => Promise<void>;
autoLaunch: (enabled: boolean) => Promise<void>;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
/* Misc */
getOrCacheImage: (url: string) => Promise<string>;
openExternal: (src: string) => Promise<void>;
getVersion: () => Promise<string>;
ping: () => string;

View file

@ -24,5 +24,7 @@ export const getSteamLanguage = (language: string) => {
if (language.startsWith("ru")) return "russian";
if (language.startsWith("it")) return "italian";
if (language.startsWith("hu")) return "hungarian";
if (language.startsWith("pl")) return "polish";
return "english";
};

View file

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
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 { useDownload, useLibrary } from "@renderer/hooks";
import type { Game } from "@types";
@ -103,6 +103,7 @@ export function Downloads() {
</>
);
}
if (game?.status === "cancelled") return <p>{t("cancelled")}</p>;
if (game?.status === "downloading_metadata")
return <p>{t("starting_download")}</p>;
@ -115,6 +116,8 @@ export function Downloads() {
</>
);
}
return null;
};
const openDeleteModal = (gameId: number) => {
@ -210,6 +213,12 @@ export function Downloads() {
);
};
const handleDeleteGame = () => {
if (gameToBeDeleted.current) {
deleteGame(gameToBeDeleted.current).then(updateLibrary);
}
};
return (
<section className={styles.downloadsContainer}>
<BinaryNotFoundModal
@ -219,9 +228,7 @@ export function Downloads() {
<DeleteModal
visible={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
deleteGame={() =>
deleteGame(gameToBeDeleted.current).then(updateLibrary)
}
deleteGame={handleDeleteGame}
/>
<TextField placeholder={t("filter")} onChange={handleFilter} />
@ -235,7 +242,7 @@ export function Downloads() {
cancelled: game.status === "cancelled",
})}
>
<AsyncImage
<img
src={steamUrlBuilder.library(game.objectID)}
className={styles.downloadCover}
alt={game.title}

View file

@ -12,7 +12,7 @@ import type {
SteamAppDetails,
} from "@types";
import { AsyncImage, Button } from "@renderer/components";
import { Button } from "@renderer/components";
import { setHeaderTitle } from "@renderer/features";
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
import { useAppDispatch, useDownload } from "@renderer/hooks";
@ -70,14 +70,16 @@ export function GameDetails() {
const { game: gameDownloading, startDownload, isDownloading } = useDownload();
const handleImageSettled = useCallback((url: string) => {
average(url, { amount: 1, format: "hex" })
const heroImage = steamUrlBuilder.libraryHero(objectID!);
const handleHeroLoad = () => {
average(heroImage, { amount: 1, format: "hex" })
.then((color) => {
const darkColor = new Color(color).darken(0.6).toString() as string;
setColor({ light: color as string, dark: darkColor });
})
.catch(() => {});
}, []);
};
const getGame = useCallback(() => {
window.electron
@ -218,15 +220,15 @@ export function GameDetails() {
) : (
<section className={styles.container}>
<div className={styles.hero}>
<AsyncImage
src={steamUrlBuilder.libraryHero(objectID!)}
<img
src={heroImage}
className={styles.heroImage}
alt={game?.title}
onSettled={handleImageSettled}
onLoad={handleHeroLoad}
/>
<div className={styles.heroBackdrop}>
<div className={styles.heroContent}>
<AsyncImage
<img
src={steamUrlBuilder.logo(objectID!)}
style={{ width: 300, alignSelf: "flex-end" }}
/>

View file

@ -68,7 +68,7 @@ export function HeroPanelActions({
try {
if (game) {
await removeGameFromLibrary(game.id);
} else {
} else if (gameDetails) {
const gameExecutablePath = await selectGameExecutable();
await window.electron.addGameToLibrary(
@ -87,30 +87,37 @@ export function HeroPanelActions({
};
const openGameInstaller = () => {
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
if (!isBinaryInPath) openBinaryNotFoundModal();
updateLibrary();
});
if (game) {
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
if (!isBinaryInPath) openBinaryNotFoundModal();
updateLibrary();
});
}
};
const openGame = async () => {
if (game.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
if (game) {
if (game.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
if (game?.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
if (game?.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
const gameExecutablePath = await selectGameExecutable();
window.electron.openGame(game.id, gameExecutablePath);
const gameExecutablePath = await selectGameExecutable();
if (gameExecutablePath)
window.electron.openGame(game.id, gameExecutablePath);
}
};
const closeGame = () => window.electron.closeGame(game.id);
const closeGame = () => {
if (game) window.electron.closeGame(game.id);
};
const deleting = isGameDeleting(game?.id);
const deleting = game ? isGameDeleting(game?.id) : false;
const toggleGameOnLibraryButton = (
<Button
@ -124,7 +131,7 @@ export function HeroPanelActions({
</Button>
);
if (isGameDownloading) {
if (game && isGameDownloading) {
return (
<>
<Button

View file

@ -98,7 +98,7 @@ export function HeroPanel({
return <p>{t("deleting")}</p>;
}
if (isGameDownloading) {
if (isGameDownloading && gameDownloading?.status) {
return (
<>
<p className={styles.downloadDetailsRow}>
@ -106,14 +106,14 @@ export function HeroPanel({
{eta && <small>{t("eta", { eta })}</small>}
</p>
{gameDownloading?.status !== "downloading" ? (
{gameDownloading.status !== "downloading" ? (
<>
<p>{t(gameDownloading?.status)}</p>
<p>{t(gameDownloading.status)}</p>
{eta && <small>{t("eta", { eta })}</small>}
</>
) : (
<p className={styles.downloadDetailsRow}>
{formatBytes(gameDownloading?.bytesDownloaded)} /{" "}
{formatBytes(gameDownloading.bytesDownloaded)} /{" "}
{finalDownloadSize}
<small>
{numPeers} peers / {numSeeds} seeds
@ -148,7 +148,7 @@ export function HeroPanel({
<>
<p>
{t("play_time", {
amount: formatPlayTime(game.playTimeInMilliseconds),
amount: formatPlayTime(),
})}
</p>

View file

@ -89,7 +89,9 @@ export function RepacksModal({
<p style={{ color: "#DADBE1" }}>{repack.title}</p>
<p style={{ fontSize: "12px" }}>
{repack.fileSize} - {repackersFriendlyNames[repack.repacker]} -{" "}
{format(repack.uploadDate, "dd/MM/yyyy")}
{repack.uploadDate
? format(repack.uploadDate, "dd/MM/yyyy")
: ""}
</p>
</Button>
))}

View file

@ -12,6 +12,7 @@ export function Settings() {
repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false,
preferQuitInsteadOfHiding: false,
runAtStartup: false,
});
const { t } = useTranslation("settings");
@ -30,6 +31,7 @@ export function Settings() {
telemetryEnabled: userPreferences?.telemetryEnabled ?? false,
preferQuitInsteadOfHiding:
userPreferences?.preferQuitInsteadOfHiding ?? false,
runAtStartup: userPreferences?.runAtStartup ?? false,
});
});
}, []);
@ -123,6 +125,15 @@ export function Settings() {
)
}
/>
<CheckboxField
label={t("launch_with_system")}
onChange={() => {
updateUserPreferences("runAtStartup", !form.runAtStartup);
window.electron.autoLaunch(!form.runAtStartup);
}}
checked={form.runAtStartup}
/>
</div>
</section>
);