mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
fix: fixing multiple state update when scrolling
This commit is contained in:
parent
d2aef7ca98
commit
c68cb3211d
@ -7,6 +7,7 @@ globalStyle("*", {
|
||||
|
||||
globalStyle("::-webkit-scrollbar", {
|
||||
width: "9px",
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
});
|
||||
|
||||
globalStyle("::-webkit-scrollbar-track", {
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
} from "@renderer/hooks";
|
||||
|
||||
import * as styles from "./app.css";
|
||||
import { themeClass } from "./theme.css";
|
||||
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
@ -21,8 +20,6 @@ import {
|
||||
closeToast,
|
||||
} from "@renderer/features";
|
||||
|
||||
document.body.classList.add(themeClass);
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { keyframes } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
export const backdropFadeIn = keyframes({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
export const badge = style({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const bottomPanel = style({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const checkboxField = style({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const card = style({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const hero = style({
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const scaleFadeIn = keyframes({
|
||||
|
@ -14,6 +14,7 @@ export interface ModalProps {
|
||||
onClose: () => void;
|
||||
large?: boolean;
|
||||
children: React.ReactNode;
|
||||
clickOutsideToClose?: boolean;
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
@ -23,6 +24,7 @@ export function Modal({
|
||||
onClose,
|
||||
large,
|
||||
children,
|
||||
clickOutsideToClose = true,
|
||||
}: ModalProps) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const modalContentRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -60,6 +62,18 @@ export function Modal({
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [handleCloseClick, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (clickOutsideToClose) {
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (!isTopMostModal()) return;
|
||||
if (modalContentRef.current) {
|
||||
@ -73,17 +87,15 @@ export function Modal({
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("mousedown", onMouseDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("mousedown", onMouseDown);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [handleCloseClick, visible]);
|
||||
}, [clickOutsideToClose, handleCloseClick]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const select = recipe({
|
||||
base: {
|
||||
display: "inline-flex",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AppsIcon, GearIcon, HomeIcon } from "@primer/octicons-react";
|
||||
|
||||
import { DownloadIcon } from "./download-icon";
|
||||
|
||||
export const routes = [
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const sidebar = recipe({
|
||||
@ -11,6 +12,7 @@ export const sidebar = recipe({
|
||||
transition: "opacity ease 0.2s",
|
||||
borderRight: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
},
|
||||
variants: {
|
||||
resizing: {
|
||||
@ -123,3 +125,46 @@ export const section = style({
|
||||
flexDirection: "column",
|
||||
paddingBottom: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const profileButton = style({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.1s",
|
||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
alignItems: "center",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
color: vars.color.muted,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const profileButtonInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
});
|
||||
|
||||
export const statusBadge = style({
|
||||
width: "9px",
|
||||
height: "9px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: vars.color.danger,
|
||||
position: "absolute",
|
||||
bottom: "-2px",
|
||||
right: "-3px",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ import * as styles from "./sidebar.css";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { PersonIcon } from "@primer/octicons-react";
|
||||
|
||||
const SIDEBAR_MIN_WIDTH = 200;
|
||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||
@ -143,93 +144,114 @@ export function Sidebar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
className={styles.sidebar({ resizing: isResizing })}
|
||||
style={{
|
||||
width: sidebarWidth,
|
||||
minWidth: sidebarWidth,
|
||||
maxWidth: sidebarWidth,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.content({
|
||||
macos: window.electron.platform === "darwin",
|
||||
})}
|
||||
<>
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
className={styles.sidebar({ resizing: isResizing })}
|
||||
style={{
|
||||
width: sidebarWidth,
|
||||
minWidth: sidebarWidth,
|
||||
maxWidth: sidebarWidth,
|
||||
}}
|
||||
>
|
||||
{window.electron.platform === "darwin" && <h2>Hydra</h2>}
|
||||
<button type="button" className={styles.profileButton}>
|
||||
<div className={styles.profileAvatar}>
|
||||
{/* <PersonIcon /> */}
|
||||
<img
|
||||
src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.pinimg.com%2F736x%2Fbd%2F19%2F2f%2Fbd192f2723f7d81013f04903d9e0428b.jpg&f=1&nofb=1&ipt=dce89b7ad791596a8b78b07c3c27f3f54fbf608493fb7217b4eb4ba4ca7904d1&ipo=images"
|
||||
alt="Avatar"
|
||||
style={{ width: "100%", height: "100%", borderRadius: "50%" }}
|
||||
/>
|
||||
|
||||
<section className={styles.section}>
|
||||
<ul className={styles.menu}>
|
||||
{routes.map(({ nameKey, path, render }) => (
|
||||
<li
|
||||
key={nameKey}
|
||||
className={styles.menuItem({
|
||||
active: location.pathname === path,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={() => handleSidebarItemClick(path)}
|
||||
<div className={styles.statusBadge} />
|
||||
</div>
|
||||
|
||||
<div className={styles.profileButtonInformation}>
|
||||
<p style={{ fontWeight: "bold" }}>hydra</p>
|
||||
<p style={{ fontSize: 12 }}>Jogando ABC</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={styles.content({
|
||||
macos: window.electron.platform === "darwin",
|
||||
})}
|
||||
>
|
||||
{window.electron.platform === "darwin" && <h2>Hydra</h2>}
|
||||
|
||||
<section className={styles.section}>
|
||||
<ul className={styles.menu}>
|
||||
{routes.map(({ nameKey, path, render }) => (
|
||||
<li
|
||||
key={nameKey}
|
||||
className={styles.menuItem({
|
||||
active: location.pathname === path,
|
||||
})}
|
||||
>
|
||||
{render(isDownloading)}
|
||||
<span>{t(nameKey)}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={() => handleSidebarItemClick(path)}
|
||||
>
|
||||
{render(isDownloading)}
|
||||
<span>{t(nameKey)}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
||||
<section className={styles.section}>
|
||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
||||
|
||||
<TextField
|
||||
placeholder={t("filter")}
|
||||
onChange={handleFilter}
|
||||
theme="dark"
|
||||
/>
|
||||
<TextField
|
||||
placeholder={t("filter")}
|
||||
onChange={handleFilter}
|
||||
theme="dark"
|
||||
/>
|
||||
|
||||
<ul className={styles.menu}>
|
||||
{filteredLibrary.map((game) => (
|
||||
<li
|
||||
key={game.id}
|
||||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === "removed",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||
<ul className={styles.menu}>
|
||||
{filteredLibrary.map((game) => (
|
||||
<li
|
||||
key={game.id}
|
||||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname ===
|
||||
`/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === "removed",
|
||||
})}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
className={styles.gameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className={styles.gameIcon} />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
className={styles.gameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className={styles.gameIcon} />
|
||||
)}
|
||||
|
||||
<span className={styles.menuItemButtonLabel}>
|
||||
{getGameTitle(game)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<span className={styles.menuItemButtonLabel}>
|
||||
{getGameTitle(game)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.handle}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
</aside>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.handle}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const textFieldContainer = style({
|
||||
flex: "1",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
|
@ -8,32 +8,7 @@ import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks";
|
||||
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
DODIInstallationGuide,
|
||||
DONT_SHOW_DODI_INSTRUCTIONS_KEY,
|
||||
DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY,
|
||||
OnlineFixInstallationGuide,
|
||||
RepacksModal,
|
||||
} from "./modals";
|
||||
import { Downloader } from "@shared";
|
||||
import { GameOptionsModal } from "./modals/game-options-modal";
|
||||
|
||||
export interface GameDetailsContext {
|
||||
game: Game | null;
|
||||
shopDetails: ShopDetails | null;
|
||||
repacks: GameRepack[];
|
||||
shop: GameShop;
|
||||
gameTitle: string;
|
||||
isGameRunning: boolean;
|
||||
isLoading: boolean;
|
||||
objectID: string | undefined;
|
||||
gameColor: string;
|
||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
openRepacksModal: () => void;
|
||||
openGameOptionsModal: () => void;
|
||||
selectGameExecutable: () => Promise<string | null>;
|
||||
updateGame: () => Promise<void>;
|
||||
}
|
||||
import { GameDetailsContext } from "./game-details.context.types";
|
||||
|
||||
export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||
game: null,
|
||||
@ -45,11 +20,13 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||
isLoading: false,
|
||||
objectID: undefined,
|
||||
gameColor: "",
|
||||
showRepacksModal: false,
|
||||
showGameOptionsModal: false,
|
||||
setGameColor: () => {},
|
||||
openRepacksModal: () => {},
|
||||
openGameOptionsModal: () => {},
|
||||
selectGameExecutable: async () => null,
|
||||
updateGame: async () => {},
|
||||
setShowGameOptionsModal: () => {},
|
||||
setShowRepacksModal: () => {},
|
||||
});
|
||||
|
||||
const { Provider } = gameDetailsContext;
|
||||
@ -70,9 +47,6 @@ export function GameDetailsContextProvider({
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [gameColor, setGameColor] = useState("");
|
||||
const [showInstructionsModal, setShowInstructionsModal] = useState<
|
||||
null | "onlinefix" | "DODI"
|
||||
>(null);
|
||||
const [isGameRunning, setisGameRunning] = useState(false);
|
||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
||||
@ -85,7 +59,7 @@ export function GameDetailsContextProvider({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { startDownload, lastPacket } = useDownload();
|
||||
const { lastPacket } = useDownload();
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
@ -152,37 +126,6 @@ export function GameDetailsContextProvider({
|
||||
};
|
||||
}, [game?.id, isGameRunning, updateGame]);
|
||||
|
||||
const handleStartDownload = async (
|
||||
repack: GameRepack,
|
||||
downloader: Downloader,
|
||||
downloadPath: string
|
||||
) => {
|
||||
await startDownload({
|
||||
repackId: repack.id,
|
||||
objectID: objectID!,
|
||||
title: gameTitle,
|
||||
downloader,
|
||||
shop: shop as GameShop,
|
||||
downloadPath,
|
||||
});
|
||||
|
||||
await updateGame();
|
||||
setShowRepacksModal(false);
|
||||
setShowGameOptionsModal(false);
|
||||
|
||||
if (
|
||||
repack.repacker === "onlinefix" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("onlinefix");
|
||||
} else if (
|
||||
repack.repacker === "DODI" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("DODI");
|
||||
}
|
||||
};
|
||||
|
||||
const getDownloadsPath = async () => {
|
||||
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||
return window.electron.getDefaultDownloadsPath();
|
||||
@ -211,9 +154,6 @@ export function GameDetailsContextProvider({
|
||||
});
|
||||
};
|
||||
|
||||
const openRepacksModal = () => setShowRepacksModal(true);
|
||||
const openGameOptionsModal = () => setShowGameOptionsModal(true);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
@ -226,42 +166,16 @@ export function GameDetailsContextProvider({
|
||||
isLoading,
|
||||
objectID,
|
||||
gameColor,
|
||||
showGameOptionsModal,
|
||||
showRepacksModal,
|
||||
setGameColor,
|
||||
openRepacksModal,
|
||||
openGameOptionsModal,
|
||||
selectGameExecutable,
|
||||
updateGame,
|
||||
setShowRepacksModal,
|
||||
setShowGameOptionsModal,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<RepacksModal
|
||||
visible={showRepacksModal}
|
||||
startDownload={handleStartDownload}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
|
||||
<OnlineFixInstallationGuide
|
||||
visible={showInstructionsModal === "onlinefix"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
<DODIInstallationGuide
|
||||
visible={showInstructionsModal === "DODI"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
{game && (
|
||||
<GameOptionsModal
|
||||
visible={showGameOptionsModal}
|
||||
game={game}
|
||||
onClose={() => {
|
||||
setShowGameOptionsModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
||||
|
||||
export interface GameDetailsContext {
|
||||
game: Game | null;
|
||||
shopDetails: ShopDetails | null;
|
||||
repacks: GameRepack[];
|
||||
shop: GameShop;
|
||||
gameTitle: string;
|
||||
isGameRunning: boolean;
|
||||
isLoading: boolean;
|
||||
objectID: string | undefined;
|
||||
gameColor: string;
|
||||
showRepacksModal: boolean;
|
||||
showGameOptionsModal: boolean;
|
||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
selectGameExecutable: () => Promise<string | null>;
|
||||
updateGame: () => Promise<void>;
|
||||
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setShowGameOptionsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
1
src/renderer/src/context/index.ts
Normal file
1
src/renderer/src/context/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./game-details/game-details.context";
|
@ -6,13 +6,14 @@ import type { CatalogueEntry } from "@types";
|
||||
|
||||
import { clearSearch } from "@renderer/features";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import * as styles from "../home/home.css";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
|
||||
export function Catalogue() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
export const deleteActionsButtonsCtn = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const downloadTitleWrapper = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@ -71,7 +72,7 @@ export const download = style({
|
||||
borderRadius: "8px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
overflow: "hidden",
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
boxShadow: "0px 0px 5px 0px #000000",
|
||||
transition: "all ease 0.2s",
|
||||
height: "140px",
|
||||
minHeight: "140px",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
export const downloadsContainer = style({
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const descriptionHeader = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
height: "72px",
|
||||
});
|
||||
|
||||
export const descriptionHeaderInfo = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
});
|
@ -1,8 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./game-details.css";
|
||||
import * as styles from "./description-header.css";
|
||||
import { useContext } from "react";
|
||||
import { gameDetailsContext } from "./game-details.context";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
export function DescriptionHeader() {
|
||||
const { shopDetails } = useContext(gameDetailsContext);
|
@ -1,7 +1,8 @@
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const gallerySliderContainer = style({
|
||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
||||
|
||||
import * as styles from "./gallery-slider.css";
|
||||
import { gameDetailsContext } from "./game-details.context";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
export function GallerySlider() {
|
||||
const { shopDetails } = useContext(gameDetailsContext);
|
116
src/renderer/src/pages/game-details/game-details-content.tsx
Normal file
116
src/renderer/src/pages/game-details/game-details-content.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { average } from "color.js";
|
||||
import Color from "color";
|
||||
|
||||
import { steamUrlBuilder } from "@renderer/helpers";
|
||||
|
||||
import { HeroPanel } from "./hero";
|
||||
import { DescriptionHeader } from "./description-header/description-header";
|
||||
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||
import { Sidebar } from "./sidebar/sidebar";
|
||||
|
||||
import * as styles from "./game-details.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
export function GameDetailsContent() {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { objectID, shopDetails, game, gameColor, setGameColor } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
||||
|
||||
const handleHeroLoad = async () => {
|
||||
const output = await average(steamUrlBuilder.libraryHero(objectID!), {
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
});
|
||||
|
||||
const backgroundColor = output
|
||||
? (new Color(output).darken(0.7).toString() as string)
|
||||
: "";
|
||||
|
||||
setGameColor(backgroundColor);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setBackdropOpacity(1);
|
||||
}, [objectID]);
|
||||
|
||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
||||
const scrollY = (event.target as HTMLDivElement).scrollTop;
|
||||
const opacity = Math.max(0, 1 - scrollY / styles.HERO_HEIGHT);
|
||||
|
||||
if (scrollY >= styles.HERO_HEIGHT && !isHeaderStuck) {
|
||||
setIsHeaderStuck(true);
|
||||
}
|
||||
|
||||
if (scrollY <= styles.HERO_HEIGHT && isHeaderStuck) {
|
||||
setIsHeaderStuck(false);
|
||||
}
|
||||
|
||||
setBackdropOpacity(opacity);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectID!)}
|
||||
className={styles.heroImage}
|
||||
alt={game?.title}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
|
||||
<section
|
||||
ref={containerRef}
|
||||
onScroll={onScroll}
|
||||
className={styles.container}
|
||||
>
|
||||
<div className={styles.hero}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: gameColor,
|
||||
flex: 1,
|
||||
opacity: Math.min(1, 1 - backdropOpactiy),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={styles.heroLogoBackdrop}
|
||||
style={{ opacity: backdropOpactiy }}
|
||||
>
|
||||
<div className={styles.heroContent}>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectID!)}
|
||||
style={{ width: 300, alignSelf: "flex-end" }}
|
||||
alt={game?.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeroPanel isHeaderStuck={isHeaderStuck} />
|
||||
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
<DescriptionHeader />
|
||||
<GallerySlider />
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: shopDetails?.about_the_game ?? t("no_shop_details"),
|
||||
}}
|
||||
className={styles.description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Sidebar />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ import { Button } from "@renderer/components";
|
||||
|
||||
import * as styles from "./game-details.css";
|
||||
import * as sidebarStyles from "./sidebar/sidebar.css";
|
||||
import * as descriptionHeaderStyles from "./description-header/description-header.css";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -16,15 +17,15 @@ export function GameDetailsSkeleton() {
|
||||
<Skeleton className={styles.heroImageSkeleton} />
|
||||
</div>
|
||||
<div className={styles.heroPanelSkeleton}>
|
||||
<section className={styles.descriptionHeaderInfo}>
|
||||
<section className={descriptionHeaderStyles.descriptionHeaderInfo}>
|
||||
<Skeleton width={155} />
|
||||
<Skeleton width={135} />
|
||||
</section>
|
||||
</div>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
<div className={styles.descriptionHeader}>
|
||||
<section className={styles.descriptionHeaderInfo}>
|
||||
<div className={descriptionHeaderStyles.descriptionHeader}>
|
||||
<section className={descriptionHeaderStyles.descriptionHeaderInfo}>
|
||||
<Skeleton width={145} />
|
||||
<Skeleton width={150} />
|
||||
</section>
|
||||
|
@ -1,15 +1,26 @@
|
||||
import { globalStyle, keyframes, style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const HERO_HEIGHT = 300;
|
||||
|
||||
export const slideIn = keyframes({
|
||||
"0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)` },
|
||||
"100%": { transform: "translateY(0)" },
|
||||
});
|
||||
|
||||
export const wrapper = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
export const hero = style({
|
||||
width: "100%",
|
||||
height: "300px",
|
||||
minHeight: "300px",
|
||||
height: `${HERO_HEIGHT}px`,
|
||||
minHeight: `${HERO_HEIGHT}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
@ -29,7 +40,7 @@ export const heroContent = style({
|
||||
display: "flex",
|
||||
});
|
||||
|
||||
export const heroBackdrop = style({
|
||||
export const heroLogoBackdrop = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 100%)",
|
||||
@ -41,13 +52,18 @@ export const heroBackdrop = style({
|
||||
|
||||
export const heroImage = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
height: `${HERO_HEIGHT}px`,
|
||||
minHeight: `${HERO_HEIGHT}px`,
|
||||
objectFit: "cover",
|
||||
objectPosition: "top",
|
||||
transition: "all ease 0.2s",
|
||||
position: "absolute",
|
||||
zIndex: "0",
|
||||
"@media": {
|
||||
"(min-width: 1250px)": {
|
||||
objectPosition: "center",
|
||||
height: "350px",
|
||||
minHeight: "350px",
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -66,12 +82,15 @@ export const container = style({
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const descriptionContainer = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flex: "1",
|
||||
background: `linear-gradient(0deg, ${vars.color.background} 50%, ${vars.color.darkBackground} 100%)`,
|
||||
});
|
||||
|
||||
export const descriptionContent = style({
|
||||
@ -111,22 +130,6 @@ export const descriptionSkeleton = style({
|
||||
marginRight: "auto",
|
||||
});
|
||||
|
||||
export const descriptionHeader = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
height: "72px",
|
||||
});
|
||||
|
||||
export const descriptionHeaderInfo = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const randomizerButton = style({
|
||||
animationName: slideIn,
|
||||
animationDuration: "0.2s",
|
||||
|
@ -1,30 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { average } from "color.js";
|
||||
|
||||
import { Steam250Game } from "@types";
|
||||
import { GameRepack, GameShop, Steam250Game } from "@types";
|
||||
|
||||
import { Button } from "@renderer/components";
|
||||
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||
|
||||
import Lottie from "lottie-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { DescriptionHeader } from "./description-header";
|
||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||
import * as styles from "./game-details.css";
|
||||
import { HeroPanel } from "./hero";
|
||||
|
||||
import { vars } from "../../theme.css";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
import { GallerySlider } from "./gallery-slider";
|
||||
import { Sidebar } from "./sidebar/sidebar";
|
||||
import { GameDetailsContent } from "./game-details-content";
|
||||
import {
|
||||
GameDetailsContextConsumer,
|
||||
GameDetailsContextProvider,
|
||||
} from "./game-details.context";
|
||||
} from "@renderer/context";
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import { GameOptionsModal, RepacksModal } from "./modals";
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export function GameDetails() {
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
@ -34,6 +33,8 @@ export function GameDetails() {
|
||||
|
||||
const fromRandomizer = searchParams.get("fromRandomizer");
|
||||
|
||||
const { startDownload } = useDownload();
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const navigate = useNavigate();
|
||||
@ -59,17 +60,34 @@ export function GameDetails() {
|
||||
return (
|
||||
<GameDetailsContextProvider>
|
||||
<GameDetailsContextConsumer>
|
||||
{({ game, shopDetails, isLoading, setGameColor }) => {
|
||||
const handleHeroLoad = async () => {
|
||||
const output = await average(
|
||||
steamUrlBuilder.libraryHero(objectID!),
|
||||
{
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
}
|
||||
);
|
||||
{({
|
||||
isLoading,
|
||||
game,
|
||||
gameTitle,
|
||||
shop,
|
||||
showRepacksModal,
|
||||
showGameOptionsModal,
|
||||
updateGame,
|
||||
setShowRepacksModal,
|
||||
setShowGameOptionsModal,
|
||||
}) => {
|
||||
const handleStartDownload = async (
|
||||
repack: GameRepack,
|
||||
downloader: Downloader,
|
||||
downloadPath: string
|
||||
) => {
|
||||
await startDownload({
|
||||
repackId: repack.id,
|
||||
objectID: objectID!,
|
||||
title: gameTitle,
|
||||
downloader,
|
||||
shop: shop as GameShop,
|
||||
downloadPath,
|
||||
});
|
||||
|
||||
setGameColor(output as string);
|
||||
await updateGame();
|
||||
setShowRepacksModal(false);
|
||||
setShowGameOptionsModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -77,47 +95,22 @@ export function GameDetails() {
|
||||
baseColor={vars.color.background}
|
||||
highlightColor="#444"
|
||||
>
|
||||
{isLoading ? (
|
||||
<GameDetailsSkeleton />
|
||||
) : (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.hero}>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectID!)}
|
||||
className={styles.heroImage}
|
||||
alt={game?.title}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
<div className={styles.heroBackdrop}>
|
||||
<div className={styles.heroContent}>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectID!)}
|
||||
style={{ width: 300, alignSelf: "flex-end" }}
|
||||
alt={game?.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />}
|
||||
|
||||
<HeroPanel />
|
||||
<RepacksModal
|
||||
visible={showRepacksModal}
|
||||
startDownload={handleStartDownload}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
<DescriptionHeader />
|
||||
<GallerySlider />
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
shopDetails?.about_the_game ?? t("no_shop_details"),
|
||||
}}
|
||||
className={styles.description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Sidebar />
|
||||
</div>
|
||||
</section>
|
||||
{game && (
|
||||
<GameOptionsModal
|
||||
visible={showGameOptionsModal}
|
||||
game={game}
|
||||
onClose={() => {
|
||||
setShowGameOptionsModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{fromRandomizer && (
|
||||
|
@ -4,7 +4,8 @@ import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./hero-panel-actions.css";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
export function HeroPanelActions() {
|
||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||
@ -18,8 +19,8 @@ export function HeroPanelActions() {
|
||||
isGameRunning,
|
||||
objectID,
|
||||
gameTitle,
|
||||
openRepacksModal,
|
||||
openGameOptionsModal,
|
||||
setShowGameOptionsModal,
|
||||
setShowRepacksModal,
|
||||
updateGame,
|
||||
selectGameExecutable,
|
||||
} = useContext(gameDetailsContext);
|
||||
@ -74,7 +75,7 @@ export function HeroPanelActions() {
|
||||
|
||||
const showDownloadOptionsButton = (
|
||||
<Button
|
||||
onClick={openRepacksModal}
|
||||
onClick={() => setShowRepacksModal(true)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
@ -119,7 +120,7 @@ export function HeroPanelActions() {
|
||||
<div className={styles.separator} />
|
||||
|
||||
<Button
|
||||
onClick={openGameOptionsModal}
|
||||
onClick={() => setShowGameOptionsModal(true)}
|
||||
theme="outline"
|
||||
disabled={deleting || isGameRunning}
|
||||
className={styles.heroPanelAction}
|
||||
|
@ -3,9 +3,10 @@ import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { useDate, useDownload } from "@renderer/hooks";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
import { Link } from "@renderer/components";
|
||||
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
export function HeroPanelPlaytime() {
|
||||
|
@ -1,19 +1,31 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const panel = style({
|
||||
width: "100%",
|
||||
height: "72px",
|
||||
minHeight: "72px",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
transition: "all ease 0.2s",
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const panel = recipe({
|
||||
base: {
|
||||
width: "100%",
|
||||
height: "72px",
|
||||
minHeight: "72px",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
transition: "all ease 0.2s",
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
position: "sticky",
|
||||
overflow: "hidden",
|
||||
top: "0",
|
||||
zIndex: "1",
|
||||
},
|
||||
variants: {
|
||||
stuck: {
|
||||
true: {
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { format } from "date-fns";
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Color from "color";
|
||||
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
|
||||
import { HeroPanelActions } from "./hero-panel-actions";
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
export function HeroPanel() {
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
export interface HeroPanelProps {
|
||||
isHeaderStuck: boolean;
|
||||
}
|
||||
|
||||
export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { game, repacks, gameColor } = useContext(gameDetailsContext);
|
||||
@ -40,17 +46,16 @@ export function HeroPanel() {
|
||||
return <HeroPanelPlaytime />;
|
||||
};
|
||||
|
||||
const backgroundColor = gameColor
|
||||
? (new Color(gameColor).darken(0.6).toString() as string)
|
||||
: "";
|
||||
|
||||
const showProgressBar =
|
||||
(game?.status === "active" && game?.progress < 1) ||
|
||||
game?.status === "paused";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ backgroundColor }} className={styles.panel}>
|
||||
<div
|
||||
style={{ backgroundColor: gameColor }}
|
||||
className={styles.panel({ stuck: isHeaderStuck })}
|
||||
>
|
||||
<div className={styles.content}>{getInfo()}</div>
|
||||
<div className={styles.actions}>
|
||||
<HeroPanelActions />
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const container = style({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../../theme.css";
|
||||
|
||||
export const optionsContainer = style({
|
||||
|
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal, TextField } from "@renderer/components";
|
||||
import type { Game } from "@types";
|
||||
import * as styles from "./game-options-modal.css";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||
@ -21,7 +21,7 @@ export function GameOptionsModal({
|
||||
}: GameOptionsModalProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { updateGame, openRepacksModal, selectGameExecutable } =
|
||||
const { updateGame, setShowRepacksModal, selectGameExecutable } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
@ -145,7 +145,7 @@ export function GameOptionsModal({
|
||||
</div>
|
||||
<div className={styles.gameOptionRow}>
|
||||
<Button
|
||||
onClick={openRepacksModal}
|
||||
onClick={() => setShowRepacksModal(true)}
|
||||
theme="outline"
|
||||
disabled={deleting || isGameDownloading}
|
||||
>
|
||||
|
@ -1,3 +1,3 @@
|
||||
export * from "./installation-guides";
|
||||
export * from "./repacks-modal";
|
||||
export * from "./download-settings-modal";
|
||||
export * from "./game-options-modal";
|
||||
|
@ -1,4 +0,0 @@
|
||||
export const DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY =
|
||||
"dontShowOnlineFixInstructions";
|
||||
|
||||
export const DONT_SHOW_DODI_INSTRUCTIONS_KEY = "dontShowDodiInstructions";
|
@ -1,31 +0,0 @@
|
||||
import { vars } from "../../../../theme.css";
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
|
||||
export const slideIn = keyframes({
|
||||
"0%": { transform: "translateY(0)" },
|
||||
"40%": { transform: "translateY(0)" },
|
||||
"70%": { transform: "translateY(-100%)" },
|
||||
"100%": { transform: "translateY(-100%)" },
|
||||
});
|
||||
|
||||
export const windowContainer = style({
|
||||
width: "250px",
|
||||
height: "150px",
|
||||
alignSelf: "center",
|
||||
borderRadius: "2px",
|
||||
overflow: "hidden",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
});
|
||||
|
||||
export const windowContent = style({
|
||||
backgroundColor: vars.color.muted,
|
||||
height: "90%",
|
||||
animationName: slideIn,
|
||||
animationDuration: "3s",
|
||||
animationIterationCount: "infinite",
|
||||
animationTimingFunction: "ease-out",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#1c1c1c",
|
||||
});
|
@ -1,78 +0,0 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, CheckboxField, Modal } from "@renderer/components";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./dodi-installation-guide.css";
|
||||
import { ArrowUpIcon } from "@primer/octicons-react";
|
||||
import { DONT_SHOW_DODI_INSTRUCTIONS_KEY } from "./constants";
|
||||
import { gameDetailsContext } from "../../game-details.context";
|
||||
|
||||
export interface DODIInstallationGuideProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DODIInstallationGuide({
|
||||
visible,
|
||||
onClose,
|
||||
}: DODIInstallationGuideProps) {
|
||||
const { gameColor } = useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
if (dontShowAgain) {
|
||||
window.localStorage.setItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY, "1");
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("installation_instructions")}
|
||||
description={t("installation_instructions_description")}
|
||||
onClose={handleClose}
|
||||
visible={visible}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: SPACING_UNIT * 2,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{ fontFamily: "Fira Sans", marginBottom: `${SPACING_UNIT}px` }}
|
||||
>
|
||||
<Trans i18nKey="dodi_installation_instruction" ns="game_details">
|
||||
<ArrowUpIcon size={16} />
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div
|
||||
className={styles.windowContainer}
|
||||
style={{ backgroundColor: gameColor }}
|
||||
>
|
||||
<div className={styles.windowContent}>
|
||||
<ArrowUpIcon size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CheckboxField
|
||||
label={t("dont_show_it_again")}
|
||||
onChange={() => setDontShowAgain(!dontShowAgain)}
|
||||
checked={dontShowAgain}
|
||||
/>
|
||||
|
||||
<Button style={{ alignSelf: "flex-end" }} onClick={handleClose}>
|
||||
{t("got_it")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from "./online-fix-installation-guide";
|
||||
export * from "./dodi-installation-guide";
|
||||
export * from "./constants";
|
@ -1,7 +0,0 @@
|
||||
import { SPACING_UNIT } from "../../../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const passwordField = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
@ -1,104 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, CheckboxField, Modal, TextField } from "@renderer/components";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./online-fix-installation-guide.css";
|
||||
import { CopyIcon } from "@primer/octicons-react";
|
||||
import { DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY } from "./constants";
|
||||
|
||||
const ONLINE_FIX_PASSWORD = "online-fix.me";
|
||||
|
||||
export interface OnlineFixInstallationGuideProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function OnlineFixInstallationGuide({
|
||||
visible,
|
||||
onClose,
|
||||
}: OnlineFixInstallationGuideProps) {
|
||||
const [clipboardLocked, setClipboardLocked] = useState(false);
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
setClipboardLocked(true);
|
||||
|
||||
navigator.clipboard.writeText(ONLINE_FIX_PASSWORD);
|
||||
|
||||
const zero = performance.now();
|
||||
|
||||
requestAnimationFrame(function holdLock(time) {
|
||||
if (time - zero <= 3000) {
|
||||
requestAnimationFrame(holdLock);
|
||||
} else {
|
||||
setClipboardLocked(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (dontShowAgain) {
|
||||
window.localStorage.setItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY, "1");
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("installation_instructions")}
|
||||
description={t("installation_instructions_description")}
|
||||
onClose={handleClose}
|
||||
visible={visible}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: SPACING_UNIT * 2,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<p style={{ fontFamily: "Fira Sans" }}>{t("online_fix_instruction")}</p>
|
||||
<div className={styles.passwordField}>
|
||||
<TextField
|
||||
value={ONLINE_FIX_PASSWORD}
|
||||
readOnly
|
||||
disabled
|
||||
style={{ fontSize: 16 }}
|
||||
textFieldProps={{ style: { height: 45 } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{ alignSelf: "flex-end", height: 45 }}
|
||||
theme="outline"
|
||||
onClick={handleCopyToClipboard}
|
||||
disabled={clipboardLocked}
|
||||
>
|
||||
{clipboardLocked ? (
|
||||
t("copied_to_clipboard")
|
||||
) : (
|
||||
<>
|
||||
<CopyIcon />
|
||||
{t("copy_to_clipboard")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CheckboxField
|
||||
label={t("dont_show_it_again")}
|
||||
onChange={() => setDontShowAgain(!dontShowAgain)}
|
||||
checked={dontShowAgain}
|
||||
/>
|
||||
|
||||
<Button style={{ alignSelf: "flex-end" }} onClick={handleClose}>
|
||||
{t("got_it")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { SPACING_UNIT } from "../../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../../theme.css";
|
||||
|
||||
export const deleteActionsButtonsCtn = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const repacks = style({
|
||||
|
@ -7,10 +7,10 @@ import type { GameRepack } from "@types";
|
||||
|
||||
import * as styles from "./repacks-modal.css";
|
||||
|
||||
import { SPACING_UNIT } from "../../../theme.css";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { format } from "date-fns";
|
||||
import { DownloadSettingsModal } from "./download-settings-modal";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export interface RepacksModalProps {
|
||||
@ -32,7 +32,7 @@ export function RepacksModal({
|
||||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||
|
||||
const [infoHash, setInfoHash] = useState("");
|
||||
const [infoHash, setInfoHash] = useState<string | null>(null);
|
||||
|
||||
const { repacks, game } = useContext(gameDetailsContext);
|
||||
|
||||
@ -40,7 +40,7 @@ export function RepacksModal({
|
||||
|
||||
const getInfoHash = useCallback(async () => {
|
||||
const torrent = await parseTorrent(game?.uri ?? "");
|
||||
setInfoHash(torrent.infoHash ?? "");
|
||||
if (torrent.infoHash) setInfoHash(torrent.infoHash);
|
||||
}, [game]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -89,29 +89,35 @@ export function RepacksModal({
|
||||
</div>
|
||||
|
||||
<div className={styles.repacks}>
|
||||
{filteredRepacks.map((repack) => (
|
||||
<Button
|
||||
key={repack.id}
|
||||
theme="dark"
|
||||
onClick={() => handleRepackClick(repack)}
|
||||
className={styles.repackButton}
|
||||
>
|
||||
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
|
||||
{repack.title}
|
||||
</p>
|
||||
{filteredRepacks.map((repack) => {
|
||||
const isLastDownloadedOption =
|
||||
infoHash !== null &&
|
||||
repack.magnet.toLowerCase().includes(infoHash);
|
||||
|
||||
{repack.magnet.toLowerCase().includes(infoHash) && (
|
||||
<Badge>{t("last_downloaded_option")}</Badge>
|
||||
)}
|
||||
return (
|
||||
<Button
|
||||
key={repack.id}
|
||||
theme="dark"
|
||||
onClick={() => handleRepackClick(repack)}
|
||||
className={styles.repackButton}
|
||||
>
|
||||
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
|
||||
{repack.title}
|
||||
</p>
|
||||
|
||||
<p style={{ fontSize: "12px" }}>
|
||||
{repack.fileSize} - {repack.repacker} -{" "}
|
||||
{repack.uploadDate
|
||||
? format(repack.uploadDate, "dd/MM/yyyy")
|
||||
: ""}
|
||||
</p>
|
||||
</Button>
|
||||
))}
|
||||
{isLastDownloadedOption && (
|
||||
<Badge>{t("last_downloaded_option")}</Badge>
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: "12px" }}>
|
||||
{repack.fileSize} - {repack.repacker} -{" "}
|
||||
{repack.uploadDate
|
||||
? format(repack.uploadDate, "dd/MM/yyyy")
|
||||
: ""}
|
||||
</p>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { HowLongToBeatCategory } from "@types";
|
||||
import { vars } from "../../../theme.css";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
|
||||
const durationTranslation: Record<string, string> = {
|
||||
|
@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@renderer/components";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
export function Sidebar() {
|
||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
export const catalogueCategories = style({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const homeHeader = style({
|
||||
|
@ -10,7 +10,7 @@ import type { Steam250Game, CatalogueEntry } from "@types";
|
||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||
|
||||
import * as styles from "./home.css";
|
||||
import { vars } from "../../theme.css";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import Lottie from "lottie-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
|
@ -9,13 +9,14 @@ import { debounce } from "lodash";
|
||||
import { InboxIcon } from "@primer/octicons-react";
|
||||
import { clearSearch } from "@renderer/features";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { vars } from "../../theme.css";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import * as styles from "./home.css";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
export function SearchResults() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -4,9 +4,10 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
import { Button, CheckboxField, Link, TextField } from "@renderer/components";
|
||||
import * as styles from "./settings-real-debrid.css";
|
||||
import type { UserPreferences } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
|
||||
|
||||
export interface SettingsRealDebridProps {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const container = style({
|
||||
padding: "24px",
|
||||
width: "100%",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Modal } from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal } from "@renderer/components";
|
||||
|
||||
interface BinaryNotFoundModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { createTheme } from "@vanilla-extract/css";
|
||||
import { createGlobalTheme } from "@vanilla-extract/css";
|
||||
|
||||
export const SPACING_UNIT = 8;
|
||||
|
||||
export const [themeClass, vars] = createTheme({
|
||||
export const vars = createGlobalTheme(":root", {
|
||||
color: {
|
||||
background: "#1c1c1c",
|
||||
darkBackground: "#151515",
|
||||
|
Loading…
Reference in New Issue
Block a user