Merge branch 'main' of https://github.com/hydralauncher/hydra
16
src/renderer/index.html
Normal 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>
|
|
@ -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>
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 828 B |
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 697 B |
|
@ -25,3 +25,5 @@ export const AsyncImage = forwardRef<HTMLImageElement, AsyncImageProps>(
|
|||
return <img ref={ref} {...props} src={source ?? props.src} />;
|
||||
}
|
||||
);
|
||||
|
||||
AsyncImage.displayName = "AsyncImage";
|
|
@ -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} "{VERSION_CODENAME}"
|
||||
</small>
|
||||
</footer>
|
||||
);
|
|
@ -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({
|
|
@ -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";
|
||||
|
|
@ -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" },
|
|
@ -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({
|
|
@ -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}
|
||||
/>
|
|
@ -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));
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
|
@ -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;
|
||||
},
|
||||
},
|
|
@ -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()),
|
|
@ -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 };
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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";
|
|
@ -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({
|
|
@ -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";
|
||||
|
|
@ -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}
|
||||
>
|
|
@ -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`, {
|
|
@ -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
|
|
@ -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}
|
||||
>
|
|
@ -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%",
|
|
@ -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> = {
|
|
@ -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",
|
|
@ -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
|
|
@ -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,
|
||||
});
|
|
@ -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}>
|
|
@ -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",
|
|
@ -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",
|
|
@ -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 = () => {
|
|
@ -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);
|
||||
})
|
|
@ -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({
|
|
@ -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
|
@ -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;
|
||||
}
|