This commit is contained in:
lilezek 2024-04-30 10:01:52 +02:00
commit 483f8223b6
156 changed files with 2841 additions and 7759 deletions

16
src/renderer/index.html Normal file
View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
/>
</head>
<body style="background-color: #1c1c1">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -12,7 +12,7 @@ import {
import * as styles from "./app.css";
import { themeClass } from "./theme.css";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import {
setSearch,
clearSearch,
@ -22,7 +22,7 @@ import {
document.body.classList.add(themeClass);
export function App() {
export function App({ children }: any) {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary } = useLibrary();
@ -112,7 +112,7 @@ export function App() {
/>
<section ref={contentRef} className={styles.content}>
<Outlet />
{children}
</section>
</article>
</main>

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 828 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 697 B

After

Width:  |  Height:  |  Size: 697 B

Before After
Before After

View file

@ -25,3 +25,5 @@ export const AsyncImage = forwardRef<HTMLImageElement, AsyncImageProps>(
return <img ref={ref} {...props} src={source ?? props.src} />;
}
);
AsyncImage.displayName = "AsyncImage";

View file

@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { useDownload } from "@renderer/hooks";
import * as styles from "./bottom-panel.css";
import { vars } from "@renderer/theme.css";
import { vars } from "../../theme.css";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { VERSION_CODENAME } from "@renderer/constants";
@ -23,7 +23,7 @@ export function BottomPanel() {
}, []);
const status = useMemo(() => {
if (isDownloading) {
if (isDownloading && game) {
if (game.status === GameStatus.DownloadingMetadata)
return t("downloading_metadata", { title: game.title });
@ -62,7 +62,7 @@ export function BottomPanel() {
</button>
<small>
v{version} "{VERSION_CODENAME}"
v{version} &quot;{VERSION_CODENAME}&quot;
</small>
</footer>
);

View file

@ -1,4 +1,4 @@
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
export const checkboxField = style({

View file

@ -1,8 +1,8 @@
import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react";
import type { CatalogueEntry } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg";
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg";
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";

View file

@ -2,7 +2,7 @@ import type { ComplexStyleRule } from "@vanilla-extract/css";
import { keyframes, style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const slideIn = keyframes({
"0%": { transform: "translateX(20px)", opacity: "0" },

View file

@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const hero = style({
width: "100%",
@ -13,11 +13,6 @@ export const hero = style({
cursor: "pointer",
border: `solid 1px ${vars.color.borderColor}`,
zIndex: "1",
"@media": {
"(min-width: 1250px)": {
backgroundPosition: "center",
},
},
});
export const heroMedia = style({

View file

@ -6,7 +6,7 @@ import { ShopDetails } from "@types";
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
import { useTranslation } from "react-i18next";
const FEATURED_GAME_ID = "377160";
const FEATURED_GAME_ID = "253230";
export function Hero() {
const [featuredGameDetails, setFeaturedGameDetails] =
@ -36,7 +36,7 @@ export function Hero() {
>
<div className={styles.backdrop}>
<AsyncImage
src="https://cdn2.steamgriddb.com/hero/e7a7ba56b1be30e178cd52820e063396.png"
src="https://cdn2.steamgriddb.com/hero/a6115ed32394915aac1e5502382eaaea.jpg"
alt={featuredGameDetails?.name}
className={styles.heroMedia}
/>

View file

@ -41,6 +41,7 @@ export function Modal({
const isTopMostModal = () => {
const openModals = document.querySelectorAll("[role=modal]");
return (
openModals.length &&
openModals[openModals.length - 1] === modalContentRef.current
@ -48,32 +49,37 @@ export function Modal({
};
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isTopMostModal()) {
handleCloseClick();
}
};
if (visible) {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isTopMostModal()) {
handleCloseClick();
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [handleCloseClick]);
const onMouseDown = (e: MouseEvent) => {
if (!isTopMostModal()) return;
if (modalContentRef.current) {
const clickedWithinModal = modalContentRef.current.contains(
e.target as Node
);
useEffect(() => {
const onMouseDown = (e: MouseEvent) => {
if (!isTopMostModal()) return;
if (!clickedWithinModal) {
handleCloseClick();
}
}
};
const clickedOutsideContent = !modalContentRef.current.contains(
e.target as Node
);
window.addEventListener("keydown", onKeyDown);
window.addEventListener("mousedown", onMouseDown);
if (clickedOutsideContent) {
handleCloseClick();
}
};
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("mousedown", onMouseDown);
};
}
window.addEventListener("mousedown", onMouseDown);
return () => window.removeEventListener("mousedown", onMouseDown);
}, [handleCloseClick]);
return () => {};
}, [handleCloseClick, visible]);
useEffect(() => {
dispatch(toggleDragging(visible));

View file

@ -6,13 +6,14 @@ import type { Game } from "@types";
import { AsyncImage, TextField } from "@renderer/components";
import { useDownload, useLibrary } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
import { SPACING_UNIT } from "../../theme.css";
import { routes } from "./routes";
import { MarkGithubIcon } from "@primer/octicons-react";
import DiscordLogo from "@renderer/assets/discord-icon.svg";
import XLogo from "@renderer/assets/x-icon.svg";
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 { GameStatus } from "@globals";

View file

@ -1,4 +1,4 @@
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";

View file

@ -7,7 +7,7 @@ export interface TextFieldProps
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
theme?: RecipeVariants<typeof styles.textField>["theme"];
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
label?: string;
}

View file

@ -64,7 +64,6 @@ declare global {
openGame: (gameId: number, executablePath: string) => Promise<void>;
closeGame: (gameId: number) => Promise<boolean>;
removeGameFromLibrary: (gameId: number) => Promise<void>;
removeGameFromDownload: (gameId: number) => Promise<vodi>;
deleteGameFolder: (gameId: number) => Promise<unknown>;
getGameByObjectID: (objectID: string) => Promise<Game | null>;
onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer;

View file

@ -14,7 +14,10 @@ export const userPreferencesSlice = createSlice({
name: "userPreferences",
initialState,
reducers: {
setUserPreferences: (state, action: PayloadAction<UserPreferences>) => {
setUserPreferences: (
state,
action: PayloadAction<UserPreferences | null>
) => {
state.value = action.payload;
},
},

View file

@ -59,15 +59,15 @@ export function useDownload() {
deleteGame(gameId);
});
const removeGameFromDownload = (gameId: number) =>
window.electron.removeGameFromDownload(gameId).then(() => {
const removeGameFromLibrary = (gameId: number) =>
window.electron.removeGameFromLibrary(gameId).then(() => {
updateLibrary();
});
const isVerifying = GameStatus.isVerifying(lastPacket?.game.status);
const getETA = () => {
if (isVerifying || !isFinite(lastPacket?.timeRemaining)) {
if (isVerifying || !isFinite(lastPacket?.timeRemaining ?? 0)) {
return "";
}
@ -125,7 +125,7 @@ export function useDownload() {
pauseDownload,
resumeDownload,
cancelDownload,
removeGameFromDownload,
removeGameFromLibrary,
deleteGame,
isGameDeleting,
clearDownload: () => dispatch(clearDownload()),

View file

@ -12,10 +12,5 @@ export function useLibrary() {
.then((updatedLibrary) => dispatch(setLibrary(updatedLibrary)));
}, [dispatch]);
const removeGameFromLibrary = (gameId: number) =>
window.electron.removeGameFromLibrary(gameId).then(() => {
updateLibrary();
});
return { library, updateLibrary, removeGameFromLibrary };
return { library, updateLibrary };
}

View file

@ -4,7 +4,7 @@ import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { Provider } from "react-redux";
import LanguageDetector from "i18next-browser-languagedetector";
import { createHashRouter, RouterProvider } from "react-router-dom";
import { HashRouter, Route, Routes } from "react-router-dom";
import { init } from "@sentry/electron/renderer";
import { init as reactInit } from "@sentry/react";
@ -31,10 +31,10 @@ import { store } from "./store";
import * as resources from "@locales";
if (process.env.SENTRY_DSN) {
if (import.meta.env.RENDERER_VITE_SENTRY_DSN) {
init(
{
dsn: process.env.SENTRY_DSN,
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
beforeSend: async (event) => {
const userPreferences = await window.electron.getUserPreferences();
@ -46,39 +46,6 @@ if (process.env.SENTRY_DSN) {
);
}
const router = createHashRouter([
{
path: "/",
Component: App,
children: [
{
path: "/",
Component: Home,
},
{
path: "/catalogue",
Component: Catalogue,
},
{
path: "/downloads",
Component: Downloads,
},
{
path: "/game/:shop/:objectID",
Component: GameDetails,
},
{
path: "/search",
Component: SearchResults,
},
{
path: "/settings",
Component: Settings,
},
],
},
]);
i18n
.use(LanguageDetector)
.use(initReactI18next)
@ -96,7 +63,18 @@ i18n
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Provider store={store}>
<RouterProvider router={router} />
<HashRouter>
<App>
<Routes>
<Route path="/" Component={Home} />
<Route path="/catalogue" Component={Catalogue} />
<Route path="/downloads" Component={Downloads} />
<Route path="/game/:shop/:objectID" Component={GameDetails} />
<Route path="/search" Component={SearchResults} />
<Route path="/settings" Component={Settings} />
</Routes>
</App>
</HashRouter>
</Provider>
</React.StrictMode>
);

View file

@ -6,7 +6,7 @@ import type { CatalogueEntry } from "@types";
import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks";
import { vars } from "@renderer/theme.css";
import { vars } from "../../theme.css";
import { useEffect, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "../home/home.css";

View file

@ -1,4 +1,4 @@
import { SPACING_UNIT } from "@renderer/theme.css";
import { SPACING_UNIT } from "../../theme.css";
import { style } from "@vanilla-extract/css";
export const deleteActionsButtonsCtn = style({

View file

@ -1,4 +1,4 @@
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";

View file

@ -34,6 +34,7 @@ export function Downloads() {
numSeeds,
pauseDownload,
resumeDownload,
removeGameFromLibrary,
cancelDownload,
deleteGame,
isGameDeleting,
@ -53,11 +54,6 @@ export function Downloads() {
updateLibrary();
});
const removeGameFromDownload = (gameId: number) =>
window.electron.removeGameFromDownload(gameId).then(() => {
updateLibrary();
});
const getFinalDownloadSize = (game: Game) => {
const isGameDownloading = isDownloading && gameDownloading?.id === game?.id;
@ -195,7 +191,7 @@ export function Downloads() {
</Button>
<Button
onClick={() => removeGameFromDownload(game.id)}
onClick={() => removeGameFromLibrary(game.id)}
theme="outline"
disabled={deleting}
>

View file

@ -1,5 +1,5 @@
import { globalStyle, keyframes, style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const slideIn = keyframes({
"0%": { transform: `translateY(${40 + 16}px)` },
@ -246,7 +246,9 @@ globalStyle(`${description} img`, {
marginTop: `${SPACING_UNIT}px`,
marginBottom: `${SPACING_UNIT * 3}px`,
display: "block",
maxWidth: "100%",
width: "100%",
height: "auto",
objectFit: "cover",
});
globalStyle(`${description} a`, {

View file

@ -1,6 +1,6 @@
import Color from "color";
import { average } from "color.js";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import type {
@ -18,7 +18,7 @@ import { useAppDispatch, useDownload } from "@renderer/hooks";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import { vars } from "@renderer/theme.css";
import { vars } from "../../theme.css";
import Lottie from "lottie-react";
import { useTranslation } from "react-i18next";
import { SkeletonTheme } from "react-loading-skeleton";
@ -33,6 +33,7 @@ export function GameDetails() {
const { objectID, shop } = useParams();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
const [color, setColor] = useState("");
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
const [howLongToBeat, setHowLongToBeat] = useState<{
@ -53,18 +54,10 @@ export function GameDetails() {
const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const randomGameObjectID = useRef<string | null>(null);
const dispatch = useAppDispatch();
const { game: gameDownloading, startDownload, isDownloading } = useDownload();
const getRandomGame = useCallback(() => {
window.electron.getRandomGame().then((objectID) => {
randomGameObjectID.current = objectID;
});
}, []);
const handleImageSettled = useCallback((url: string) => {
average(url, { amount: 1, format: "hex" })
.then((color) => {
@ -75,7 +68,7 @@ export function GameDetails() {
const getGame = useCallback(() => {
window.electron
.getGameByObjectID(objectID)
.getGameByObjectID(objectID!)
.then((result) => setGame(result));
}, [setGame, objectID]);
@ -89,10 +82,8 @@ export function GameDetails() {
setIsGamePlaying(false);
dispatch(setHeaderTitle(""));
getRandomGame();
window.electron
.getGameShopDetails(objectID, "steam", getSteamLanguage(i18n.language))
.getGameShopDetails(objectID!, "steam", getSteamLanguage(i18n.language))
.then((result) => {
if (!result) {
navigate(-1);
@ -100,13 +91,14 @@ export function GameDetails() {
}
window.electron
.getHowLongToBeat(objectID, "steam", result.name)
.getHowLongToBeat(objectID!, "steam", result.name)
.then((data) => {
setHowLongToBeat({ isLoading: false, data });
});
setGameDetails(result);
dispatch(setHeaderTitle(result.name));
setIsLoadingRandomGame(false);
})
.finally(() => {
setIsLoading(false);
@ -114,7 +106,7 @@ export function GameDetails() {
getGame();
setHowLongToBeat({ isLoading: true, data: null });
}, [getGame, getRandomGame, dispatch, navigate, objectID, i18n.language]);
}, [getGame, dispatch, navigate, objectID, i18n.language]);
const isGameDownloading = isDownloading && gameDownloading?.id === game?.id;
@ -145,29 +137,30 @@ export function GameDetails() {
repackId: number,
downloadPath: string
) => {
return startDownload(
repackId,
gameDetails.objectID,
gameDetails.name,
shop as GameShop,
downloadPath
).then(() => {
getGame();
setShowRepacksModal(false);
setShowSelectFolderModal(false);
});
if (gameDetails) {
return startDownload(
repackId,
gameDetails.objectID,
gameDetails.name,
shop as GameShop,
downloadPath
).then(() => {
getGame();
setShowRepacksModal(false);
setShowSelectFolderModal(false);
});
}
};
const handleRandomizerClick = () => {
if (!randomGameObjectID.current) return;
const handleRandomizerClick = async () => {
setIsLoadingRandomGame(true);
const randomGameObjectID = await window.electron.getRandomGame();
const searchParams = new URLSearchParams({
fromRandomizer: "1",
});
navigate(
`/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
);
navigate(`/game/steam/${randomGameObjectID}?${searchParams.toString()}`);
};
const fromRandomizer = searchParams.get("fromRandomizer");
@ -191,7 +184,7 @@ export function GameDetails() {
<section className={styles.container}>
<div className={styles.hero}>
<AsyncImage
src={steamUrlBuilder.libraryHero(objectID)}
src={steamUrlBuilder.libraryHero(objectID!)}
className={styles.heroImage}
alt={game?.title}
onSettled={handleImageSettled}
@ -199,7 +192,7 @@ export function GameDetails() {
<div className={styles.heroBackdrop}>
<div className={styles.heroContent}>
<AsyncImage
src={steamUrlBuilder.logo(objectID)}
src={steamUrlBuilder.logo(objectID!)}
style={{ width: 300, alignSelf: "flex-end" }}
/>
</div>
@ -270,7 +263,7 @@ export function GameDetails() {
title: gameDetails?.name,
}),
}}
></div>
/>
</div>
</div>
</section>
@ -281,6 +274,7 @@ export function GameDetails() {
className={styles.randomizerButton}
onClick={handleRandomizerClick}
theme="outline"
disabled={isLoadingRandomGame}
>
<div style={{ width: 16, height: 16, position: "relative" }}>
<Lottie

View file

@ -33,11 +33,11 @@ export function HeroPanelActions({
resumeDownload,
pauseDownload,
cancelDownload,
removeGameFromDownload,
removeGameFromLibrary,
isGameDeleting,
} = useDownload();
const { updateLibrary, removeGameFromLibrary } = useLibrary();
const { updateLibrary } = useLibrary();
const { t } = useTranslation("game_details");
@ -102,9 +102,6 @@ export function HeroPanelActions({
}
const gameExecutablePath = await selectGameExecutable();
if (!gameExecutablePath) return;
window.electron.openGame(game.id, gameExecutablePath);
};
@ -191,7 +188,7 @@ export function HeroPanelActions({
{t("open_download_options")}
</Button>
<Button
onClick={() => removeGameFromDownload(game.id).then(getGame)}
onClick={() => removeGameFromLibrary(game.id).then(getGame)}
theme="outline"
disabled={deleting}
>

View file

@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const panel = style({
width: "100%",

View file

@ -1,7 +1,7 @@
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import type { HowLongToBeatCategory } from "@types";
import { useTranslation } from "react-i18next";
import { vars } from "@renderer/theme.css";
import { vars } from "../../theme.css";
import * as styles from "./game-details.css";
const durationTranslation: Record<string, string> = {

View file

@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const repacks = style({
display: "flex",

View file

@ -7,7 +7,7 @@ import type { GameRepack, ShopDetails } from "@types";
import * as styles from "./repacks-modal.css";
import { useAppSelector } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
import { SPACING_UNIT } from "../../theme.css";
import { format } from "date-fns";
import { SelectFolderModal } from "./select-folder-modal";
@ -29,7 +29,7 @@ export function RepacksModal({
onClose,
}: RepacksModalProps) {
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack>(null);
const [repack, setRepack] = useState<GameRepack | null>(null);
const repackersFriendlyNames = useAppSelector(
(state) => state.repackersFriendlyNames.value

View file

@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const container = style({
display: "flex",
@ -14,6 +14,6 @@ export const downloadsPathField = style({
});
export const hintText = style({
fontSize: 12,
fontSize: "12px",
color: vars.color.bodyText,
});

View file

@ -13,7 +13,7 @@ export interface SelectFolderModalProps {
gameDetails: ShopDetails;
onClose: () => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
repack: GameRepack;
repack: GameRepack | null;
}
export function SelectFolderModal({
@ -25,7 +25,7 @@ export function SelectFolderModal({
}: SelectFolderModalProps) {
const { t } = useTranslation("game_details");
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace>(null);
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
@ -61,10 +61,12 @@ export function SelectFolderModal({
};
const handleStartClick = () => {
setDownloadStarting(true);
startDownload(repack.id, selectedPath).finally(() => {
setDownloadStarting(false);
});
if (repack) {
setDownloadStarting(true);
startDownload(repack.id, selectedPath).finally(() => {
setDownloadStarting(false);
});
}
};
return (
@ -103,7 +105,7 @@ export function SelectFolderModal({
color: "#C0C1C7",
}}
>
{t("hydra_settings")}
{t("settings")}
</Link>
</p>
<Button onClick={handleStartClick} disabled={downloadStarting}>

View file

@ -1,6 +1,6 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT } from "@renderer/theme.css";
import { SPACING_UNIT } from "../../theme.css";
export const catalogueCategories = style({
display: "flex",

View file

@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const homeCategories = style({
display: "flex",

View file

@ -10,7 +10,7 @@ import type { CatalogueCategory, CatalogueEntry } from "@types";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import * as styles from "./home.css";
import { vars } from "@renderer/theme.css";
import { vars } from "../../theme.css";
import Lottie from "lottie-react";
const categories: CatalogueCategory[] = ["trending", "recently_added"];
@ -51,21 +51,19 @@ export function Home() {
const handleSelectCategory = (category: CatalogueCategory) => {
if (category !== currentCategory) {
getCatalogue(category);
navigate(`/?category=${category}`, { replace: true });
navigate(`/?category=${category}`);
}
};
const getRandomGame = useCallback(() => {
setIsLoadingRandomGame(true);
window.electron
.getRandomGame()
.then((objectID) => {
window.electron.getRandomGame().then((objectID) => {
if (objectID) {
randomGameObjectID.current = objectID;
})
.finally(() => {
setIsLoadingRandomGame(false);
});
}
});
}, []);
const handleRandomizerClick = () => {

View file

@ -4,12 +4,12 @@ import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import type { CatalogueEntry } from "@types";
import type { DebouncedFunc } from "lodash";
import debounce from "lodash/debounce";
import { debounce } from "lodash-es";
import { InboxIcon } from "@primer/octicons-react";
import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks";
import { vars } from "@renderer/theme.css";
import { vars } from "../../theme.css";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
@ -24,7 +24,7 @@ export function SearchResults() {
const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]);
const [isLoading, setIsLoading] = useState(false);
const debouncedFunc = useRef<DebouncedFunc<() => void | null>>(null);
const debouncedFunc = useRef<DebouncedFunc<() => void> | null>(null);
const navigate = useNavigate();
@ -39,7 +39,7 @@ export function SearchResults() {
debouncedFunc.current = debounce(() => {
window.electron
.searchGames(searchParams.get("query"))
.searchGames(searchParams.get("query") ?? "")
.then((results) => {
setSearchResults(results);
})

View file

@ -1,4 +1,4 @@
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
export const container = style({

View file

@ -24,10 +24,10 @@ export function Settings() {
setForm({
downloadsPath: userPreferences?.downloadsPath || path,
downloadNotificationsEnabled:
userPreferences?.downloadNotificationsEnabled,
userPreferences?.downloadNotificationsEnabled ?? false,
repackUpdatesNotificationsEnabled:
userPreferences?.repackUpdatesNotificationsEnabled,
telemetryEnabled: userPreferences?.telemetryEnabled,
userPreferences?.repackUpdatesNotificationsEnabled ?? false,
telemetryEnabled: userPreferences?.telemetryEnabled ?? false,
realDebridApiToken: userPreferences.realDebridApiToken,
});
});

10
src/renderer/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv {
readonly RENDERER_VITE_SENTRY_DSN: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}