Merge pull request #590 from hydralauncher/feature/adding-animation-to-game-details

Feature/adding animation to game details
This commit is contained in:
Chubby Granny Chaser 2024-06-12 17:32:20 +01:00 committed by GitHub
commit 55d1bfb34d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 531 additions and 568 deletions

View File

@ -7,6 +7,7 @@ globalStyle("*", {
globalStyle("::-webkit-scrollbar", { globalStyle("::-webkit-scrollbar", {
width: "9px", width: "9px",
backgroundColor: vars.color.darkBackground,
}); });
globalStyle("::-webkit-scrollbar-track", { globalStyle("::-webkit-scrollbar-track", {

View File

@ -10,7 +10,6 @@ import {
} from "@renderer/hooks"; } from "@renderer/hooks";
import * as styles from "./app.css"; import * as styles from "./app.css";
import { themeClass } from "./theme.css";
import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { import {
@ -21,8 +20,6 @@ import {
closeToast, closeToast,
} from "@renderer/features"; } from "@renderer/features";
document.body.classList.add(themeClass);
export interface AppProps { export interface AppProps {
children: React.ReactNode; children: React.ReactNode;
} }

View File

@ -1,5 +1,6 @@
import { keyframes } from "@vanilla-extract/css"; import { keyframes } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT } from "../../theme.css"; import { SPACING_UNIT } from "../../theme.css";
export const backdropFadeIn = keyframes({ export const backdropFadeIn = keyframes({

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const checkboxField = style({ export const checkboxField = style({
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { keyframes, style } from "@vanilla-extract/css"; import { keyframes, style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css"; import { SPACING_UNIT, vars } from "../../theme.css";
export const scaleFadeIn = keyframes({ export const scaleFadeIn = keyframes({

View File

@ -14,6 +14,7 @@ export interface ModalProps {
onClose: () => void; onClose: () => void;
large?: boolean; large?: boolean;
children: React.ReactNode; children: React.ReactNode;
clickOutsideToClose?: boolean;
} }
export function Modal({ export function Modal({
@ -23,6 +24,7 @@ export function Modal({
onClose, onClose,
large, large,
children, children,
clickOutsideToClose = true,
}: ModalProps) { }: ModalProps) {
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
const modalContentRef = useRef<HTMLDivElement | null>(null); 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) => { const onMouseDown = (e: MouseEvent) => {
if (!isTopMostModal()) return; if (!isTopMostModal()) return;
if (modalContentRef.current) { if (modalContentRef.current) {
@ -73,17 +87,15 @@ export function Modal({
} }
}; };
window.addEventListener("keydown", onKeyDown);
window.addEventListener("mousedown", onMouseDown); window.addEventListener("mousedown", onMouseDown);
return () => { return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("mousedown", onMouseDown); window.removeEventListener("mousedown", onMouseDown);
}; };
} }
return () => {}; return () => {};
}, [handleCloseClick, visible]); }, [clickOutsideToClose, handleCloseClick]);
if (!visible) return null; if (!visible) return null;

View File

@ -1,7 +1,8 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const select = recipe({ export const select = recipe({
base: { base: {
display: "inline-flex", display: "inline-flex",

View File

@ -1,4 +1,5 @@
import { AppsIcon, GearIcon, HomeIcon } from "@primer/octicons-react"; import { AppsIcon, GearIcon, HomeIcon } from "@primer/octicons-react";
import { DownloadIcon } from "./download-icon"; import { DownloadIcon } from "./download-icon";
export const routes = [ export const routes = [

View File

@ -1,5 +1,6 @@
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css"; import { SPACING_UNIT, vars } from "../../theme.css";
export const sidebar = recipe({ export const sidebar = recipe({
@ -11,6 +12,7 @@ export const sidebar = recipe({
transition: "opacity ease 0.2s", transition: "opacity ease 0.2s",
borderRight: `solid 1px ${vars.color.border}`, borderRight: `solid 1px ${vars.color.border}`,
position: "relative", position: "relative",
overflow: "hidden",
}, },
variants: { variants: {
resizing: { resizing: {
@ -123,3 +125,46 @@ export const section = style({
flexDirection: "column", flexDirection: "column",
paddingBottom: `${SPACING_UNIT}px`, 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",
});

View File

@ -13,6 +13,7 @@ import * as styles from "./sidebar.css";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { PersonIcon } from "@primer/octicons-react";
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_INITIAL_WIDTH = 250;
@ -143,93 +144,109 @@ export function Sidebar() {
}; };
return ( return (
<aside <>
ref={sidebarRef} <aside
className={styles.sidebar({ resizing: isResizing })} ref={sidebarRef}
style={{ className={styles.sidebar({ resizing: isResizing })}
width: sidebarWidth, style={{
minWidth: sidebarWidth, width: sidebarWidth,
maxWidth: sidebarWidth, minWidth: sidebarWidth,
}} maxWidth: sidebarWidth,
> }}
<div
className={styles.content({
macos: window.electron.platform === "darwin",
})}
> >
{window.electron.platform === "darwin" && <h2>Hydra</h2>} <button type="button" className={styles.profileButton}>
<div className={styles.profileAvatar}>
<PersonIcon />
<section className={styles.section}> <div className={styles.statusBadge} />
<ul className={styles.menu}> </div>
{routes.map(({ nameKey, path, render }) => (
<li <div className={styles.profileButtonInformation}>
key={nameKey} <p style={{ fontWeight: "bold" }}>hydra</p>
className={styles.menuItem({ <p style={{ fontSize: 12 }}>Jogando ABC</p>
active: location.pathname === path, </div>
})} </button>
>
<button <div
type="button" className={styles.content({
className={styles.menuItemButton} macos: window.electron.platform === "darwin",
onClick={() => handleSidebarItemClick(path)} })}
>
{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)} <button
<span>{t(nameKey)}</span> type="button"
</button> className={styles.menuItemButton}
</li> onClick={() => handleSidebarItemClick(path)}
))} >
</ul> {render(isDownloading)}
</section> <span>{t(nameKey)}</span>
</button>
</li>
))}
</ul>
</section>
<section className={styles.section}> <section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small> <small className={styles.sectionTitle}>{t("my_library")}</small>
<TextField <TextField
placeholder={t("filter")} placeholder={t("filter")}
onChange={handleFilter} onChange={handleFilter}
theme="dark" theme="dark"
/> />
<ul className={styles.menu}> <ul className={styles.menu}>
{filteredLibrary.map((game) => ( {filteredLibrary.map((game) => (
<li <li
key={game.id} key={game.id}
className={styles.menuItem({ className={styles.menuItem({
active: active:
location.pathname === `/game/${game.shop}/${game.objectID}`, location.pathname ===
muted: game.status === "removed", `/game/${game.shop}/${game.objectID}`,
})} muted: game.status === "removed",
> })}
<button
type="button"
className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)}
> >
{game.iconUrl ? ( <button
<img type="button"
className={styles.gameIcon} className={styles.menuItemButton}
src={game.iconUrl} onClick={(event) => handleSidebarGameClick(event, game)}
alt={game.title} >
/> {game.iconUrl ? (
) : ( <img
<SteamLogo className={styles.gameIcon} /> className={styles.gameIcon}
)} src={game.iconUrl}
alt={game.title}
/>
) : (
<SteamLogo className={styles.gameIcon} />
)}
<span className={styles.menuItemButtonLabel}> <span className={styles.menuItemButtonLabel}>
{getGameTitle(game)} {getGameTitle(game)}
</span> </span>
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
</section> </section>
</div> </div>
<button <button
type="button" type="button"
className={styles.handle} className={styles.handle}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
/> />
</aside> </aside>
</>
); );
} }

View File

@ -1,7 +1,8 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const textFieldContainer = style({ export const textFieldContainer = style({
flex: "1", flex: "1",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,

View File

@ -8,32 +8,7 @@ import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks";
import type { Game, GameRepack, GameShop, ShopDetails } from "@types"; import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { GameDetailsContext } from "./game-details.context.types";
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>;
}
export const gameDetailsContext = createContext<GameDetailsContext>({ export const gameDetailsContext = createContext<GameDetailsContext>({
game: null, game: null,
@ -45,11 +20,13 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
isLoading: false, isLoading: false,
objectID: undefined, objectID: undefined,
gameColor: "", gameColor: "",
showRepacksModal: false,
showGameOptionsModal: false,
setGameColor: () => {}, setGameColor: () => {},
openRepacksModal: () => {},
openGameOptionsModal: () => {},
selectGameExecutable: async () => null, selectGameExecutable: async () => null,
updateGame: async () => {}, updateGame: async () => {},
setShowGameOptionsModal: () => {},
setShowRepacksModal: () => {},
}); });
const { Provider } = gameDetailsContext; const { Provider } = gameDetailsContext;
@ -70,9 +47,6 @@ export function GameDetailsContextProvider({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [gameColor, setGameColor] = useState(""); const [gameColor, setGameColor] = useState("");
const [showInstructionsModal, setShowInstructionsModal] = useState<
null | "onlinefix" | "DODI"
>(null);
const [isGameRunning, setisGameRunning] = useState(false); const [isGameRunning, setisGameRunning] = useState(false);
const [showRepacksModal, setShowRepacksModal] = useState(false); const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false); const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
@ -85,7 +59,7 @@ export function GameDetailsContextProvider({
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { startDownload, lastPacket } = useDownload(); const { lastPacket } = useDownload();
const userPreferences = useAppSelector( const userPreferences = useAppSelector(
(state) => state.userPreferences.value (state) => state.userPreferences.value
@ -152,37 +126,6 @@ export function GameDetailsContextProvider({
}; };
}, [game?.id, isGameRunning, updateGame]); }, [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 () => { const getDownloadsPath = async () => {
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath; if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
return window.electron.getDefaultDownloadsPath(); return window.electron.getDefaultDownloadsPath();
@ -211,9 +154,6 @@ export function GameDetailsContextProvider({
}); });
}; };
const openRepacksModal = () => setShowRepacksModal(true);
const openGameOptionsModal = () => setShowGameOptionsModal(true);
return ( return (
<Provider <Provider
value={{ value={{
@ -226,42 +166,16 @@ export function GameDetailsContextProvider({
isLoading, isLoading,
objectID, objectID,
gameColor, gameColor,
showGameOptionsModal,
showRepacksModal,
setGameColor, setGameColor,
openRepacksModal,
openGameOptionsModal,
selectGameExecutable, selectGameExecutable,
updateGame, updateGame,
setShowRepacksModal,
setShowGameOptionsModal,
}} }}
> >
<> {children}
<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}
</>
</Provider> </Provider>
); );
} }

View File

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

View File

@ -0,0 +1 @@
export * from "./game-details/game-details.context";

View File

@ -6,13 +6,14 @@ import type { CatalogueEntry } from "@types";
import { clearSearch } from "@renderer/features"; import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks"; import { useAppDispatch } from "@renderer/hooks";
import { SPACING_UNIT, vars } from "../../theme.css";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "../home/home.css"; import * as styles from "../home/home.css";
import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react"; import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
export function Catalogue() { export function Catalogue() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();

View File

@ -1,6 +1,7 @@
import { SPACING_UNIT } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../theme.css";
export const deleteActionsButtonsCtn = style({ export const deleteActionsButtonsCtn = style({
display: "flex", display: "flex",
width: "100%", width: "100%",

View File

@ -1,6 +1,7 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const downloadTitleWrapper = style({ export const downloadTitleWrapper = style({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -71,7 +72,7 @@ export const download = style({
borderRadius: "8px", borderRadius: "8px",
border: `solid 1px ${vars.color.border}`, border: `solid 1px ${vars.color.border}`,
overflow: "hidden", overflow: "hidden",
boxShadow: "0px 0px 15px 0px #000000", boxShadow: "0px 0px 5px 0px #000000",
transition: "all ease 0.2s", transition: "all ease 0.2s",
height: "140px", height: "140px",
minHeight: "140px", minHeight: "140px",

View File

@ -1,6 +1,7 @@
import { SPACING_UNIT } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../theme.css";
export const downloadsContainer = style({ export const downloadsContainer = style({
display: "flex", display: "flex",
padding: `${SPACING_UNIT * 3}px`, padding: `${SPACING_UNIT * 3}px`,

View File

@ -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",
});

View File

@ -1,8 +1,8 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./game-details.css"; import * as styles from "./description-header.css";
import { useContext } from "react"; import { useContext } from "react";
import { gameDetailsContext } from "./game-details.context"; import { gameDetailsContext } from "@renderer/context";
export function DescriptionHeader() { export function DescriptionHeader() {
const { shopDetails } = useContext(gameDetailsContext); const { shopDetails } = useContext(gameDetailsContext);

View File

@ -1,7 +1,8 @@
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../../theme.css";
export const gallerySliderContainer = style({ export const gallerySliderContainer = style({
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
width: "100%", width: "100%",

View File

@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react"; import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
import * as styles from "./gallery-slider.css"; import * as styles from "./gallery-slider.css";
import { gameDetailsContext } from "./game-details.context"; import { gameDetailsContext } from "@renderer/context";
export function GallerySlider() { export function GallerySlider() {
const { shopDetails } = useContext(gameDetailsContext); const { shopDetails } = useContext(gameDetailsContext);

View 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>
);
}

View File

@ -4,6 +4,7 @@ import { Button } from "@renderer/components";
import * as styles from "./game-details.css"; import * as styles from "./game-details.css";
import * as sidebarStyles from "./sidebar/sidebar.css"; import * as sidebarStyles from "./sidebar/sidebar.css";
import * as descriptionHeaderStyles from "./description-header/description-header.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -16,15 +17,15 @@ export function GameDetailsSkeleton() {
<Skeleton className={styles.heroImageSkeleton} /> <Skeleton className={styles.heroImageSkeleton} />
</div> </div>
<div className={styles.heroPanelSkeleton}> <div className={styles.heroPanelSkeleton}>
<section className={styles.descriptionHeaderInfo}> <section className={descriptionHeaderStyles.descriptionHeaderInfo}>
<Skeleton width={155} /> <Skeleton width={155} />
<Skeleton width={135} /> <Skeleton width={135} />
</section> </section>
</div> </div>
<div className={styles.descriptionContainer}> <div className={styles.descriptionContainer}>
<div className={styles.descriptionContent}> <div className={styles.descriptionContent}>
<div className={styles.descriptionHeader}> <div className={descriptionHeaderStyles.descriptionHeader}>
<section className={styles.descriptionHeaderInfo}> <section className={descriptionHeaderStyles.descriptionHeaderInfo}>
<Skeleton width={145} /> <Skeleton width={145} />
<Skeleton width={150} /> <Skeleton width={150} />
</section> </section>

View File

@ -1,15 +1,26 @@
import { globalStyle, keyframes, style } from "@vanilla-extract/css"; import { globalStyle, keyframes, style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css"; import { SPACING_UNIT, vars } from "../../theme.css";
export const HERO_HEIGHT = 300;
export const slideIn = keyframes({ export const slideIn = keyframes({
"0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)` }, "0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)` },
"100%": { transform: "translateY(0)" }, "100%": { transform: "translateY(0)" },
}); });
export const wrapper = style({
display: "flex",
flexDirection: "column",
overflow: "hidden",
width: "100%",
height: "100%",
});
export const hero = style({ export const hero = style({
width: "100%", width: "100%",
height: "300px", height: `${HERO_HEIGHT}px`,
minHeight: "300px", minHeight: `${HERO_HEIGHT}px`,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
position: "relative", position: "relative",
@ -29,7 +40,7 @@ export const heroContent = style({
display: "flex", display: "flex",
}); });
export const heroBackdrop = style({ export const heroLogoBackdrop = style({
width: "100%", width: "100%",
height: "100%", height: "100%",
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 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({ export const heroImage = style({
width: "100%", width: "100%",
height: "100%", height: `${HERO_HEIGHT}px`,
minHeight: `${HERO_HEIGHT}px`,
objectFit: "cover", objectFit: "cover",
objectPosition: "top", objectPosition: "top",
transition: "all ease 0.2s", transition: "all ease 0.2s",
position: "absolute",
zIndex: "0",
"@media": { "@media": {
"(min-width: 1250px)": { "(min-width: 1250px)": {
objectPosition: "center", objectPosition: "center",
height: "350px",
minHeight: "350px",
}, },
}, },
}); });
@ -66,12 +82,15 @@ export const container = style({
height: "100%", height: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
overflow: "auto",
zIndex: "1",
}); });
export const descriptionContainer = style({ export const descriptionContainer = style({
display: "flex", display: "flex",
width: "100%", width: "100%",
flex: "1", flex: "1",
background: `linear-gradient(0deg, ${vars.color.background} 50%, ${vars.color.darkBackground} 100%)`,
}); });
export const descriptionContent = style({ export const descriptionContent = style({
@ -111,22 +130,6 @@ export const descriptionSkeleton = style({
marginRight: "auto", 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({ export const randomizerButton = style({
animationName: slideIn, animationName: slideIn,
animationDuration: "0.2s", animationDuration: "0.2s",

View File

@ -1,30 +1,29 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; 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 { Button } from "@renderer/components";
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import starsAnimation from "@renderer/assets/lottie/stars.json"; import starsAnimation from "@renderer/assets/lottie/stars.json";
import Lottie from "lottie-react"; import Lottie from "lottie-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SkeletonTheme } from "react-loading-skeleton"; import { SkeletonTheme } from "react-loading-skeleton";
import { DescriptionHeader } from "./description-header";
import { GameDetailsSkeleton } from "./game-details-skeleton"; import { GameDetailsSkeleton } from "./game-details-skeleton";
import * as styles from "./game-details.css"; 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 { GameDetailsContent } from "./game-details-content";
import { Sidebar } from "./sidebar/sidebar";
import { import {
GameDetailsContextConsumer, GameDetailsContextConsumer,
GameDetailsContextProvider, 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() { export function GameDetails() {
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null); const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
@ -34,6 +33,8 @@ export function GameDetails() {
const fromRandomizer = searchParams.get("fromRandomizer"); const fromRandomizer = searchParams.get("fromRandomizer");
const { startDownload } = useDownload();
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
const navigate = useNavigate(); const navigate = useNavigate();
@ -59,17 +60,34 @@ export function GameDetails() {
return ( return (
<GameDetailsContextProvider> <GameDetailsContextProvider>
<GameDetailsContextConsumer> <GameDetailsContextConsumer>
{({ game, shopDetails, isLoading, setGameColor }) => { {({
const handleHeroLoad = async () => { isLoading,
const output = await average( game,
steamUrlBuilder.libraryHero(objectID!), gameTitle,
{ shop,
amount: 1, showRepacksModal,
format: "hex", 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 ( return (
@ -77,47 +95,22 @@ export function GameDetails() {
baseColor={vars.color.background} baseColor={vars.color.background}
highlightColor="#444" highlightColor="#444"
> >
{isLoading ? ( {isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />}
<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>
<HeroPanel /> <RepacksModal
visible={showRepacksModal}
startDownload={handleStartDownload}
onClose={() => setShowRepacksModal(false)}
/>
<div className={styles.descriptionContainer}> {game && (
<div className={styles.descriptionContent}> <GameOptionsModal
<DescriptionHeader /> visible={showGameOptionsModal}
<GallerySlider /> game={game}
onClose={() => {
<div setShowGameOptionsModal(false);
dangerouslySetInnerHTML={{ }}
__html: />
shopDetails?.about_the_game ?? t("no_shop_details"),
}}
className={styles.description}
/>
</div>
<Sidebar />
</div>
</section>
)} )}
{fromRandomizer && ( {fromRandomizer && (

View File

@ -4,7 +4,8 @@ import { useDownload, useLibrary } from "@renderer/hooks";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./hero-panel-actions.css"; import * as styles from "./hero-panel-actions.css";
import { gameDetailsContext } from "../game-details.context";
import { gameDetailsContext } from "@renderer/context";
export function HeroPanelActions() { export function HeroPanelActions() {
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] = const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
@ -18,8 +19,8 @@ export function HeroPanelActions() {
isGameRunning, isGameRunning,
objectID, objectID,
gameTitle, gameTitle,
openRepacksModal, setShowGameOptionsModal,
openGameOptionsModal, setShowRepacksModal,
updateGame, updateGame,
selectGameExecutable, selectGameExecutable,
} = useContext(gameDetailsContext); } = useContext(gameDetailsContext);
@ -74,7 +75,7 @@ export function HeroPanelActions() {
const showDownloadOptionsButton = ( const showDownloadOptionsButton = (
<Button <Button
onClick={openRepacksModal} onClick={() => setShowRepacksModal(true)}
theme="outline" theme="outline"
disabled={deleting} disabled={deleting}
className={styles.heroPanelAction} className={styles.heroPanelAction}
@ -119,7 +120,7 @@ export function HeroPanelActions() {
<div className={styles.separator} /> <div className={styles.separator} />
<Button <Button
onClick={openGameOptionsModal} onClick={() => setShowGameOptionsModal(true)}
theme="outline" theme="outline"
disabled={deleting || isGameRunning} disabled={deleting || isGameRunning}
className={styles.heroPanelAction} className={styles.heroPanelAction}

View File

@ -3,9 +3,10 @@ import { useTranslation } from "react-i18next";
import * as styles from "./hero-panel.css"; import * as styles from "./hero-panel.css";
import { formatDownloadProgress } from "@renderer/helpers"; import { formatDownloadProgress } from "@renderer/helpers";
import { useDate, useDownload } from "@renderer/hooks"; import { useDate, useDownload } from "@renderer/hooks";
import { gameDetailsContext } from "../game-details.context";
import { Link } from "@renderer/components"; import { Link } from "@renderer/components";
import { gameDetailsContext } from "@renderer/context";
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120; const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
export function HeroPanelPlaytime() { export function HeroPanelPlaytime() {

View File

@ -1,19 +1,31 @@
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../../theme.css";
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
export const panel = style({ import { SPACING_UNIT, vars } from "../../../theme.css";
width: "100%",
height: "72px", export const panel = recipe({
minHeight: "72px", base: {
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`, width: "100%",
display: "flex", height: "72px",
alignItems: "center", minHeight: "72px",
justifyContent: "space-between", padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
transition: "all ease 0.2s", display: "flex",
borderBottom: `solid 1px ${vars.color.border}`, alignItems: "center",
position: "relative", justifyContent: "space-between",
overflow: "hidden", 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({ export const content = style({

View File

@ -1,14 +1,20 @@
import { format } from "date-fns"; import { format } from "date-fns";
import { useContext } from "react"; import { useContext } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Color from "color";
import { useDownload } from "@renderer/hooks"; import { useDownload } from "@renderer/hooks";
import { HeroPanelActions } from "./hero-panel-actions"; import { HeroPanelActions } from "./hero-panel-actions";
import * as styles from "./hero-panel.css"; import * as styles from "./hero-panel.css";
import { HeroPanelPlaytime } from "./hero-panel-playtime"; 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 { t } = useTranslation("game_details");
const { game, repacks, gameColor } = useContext(gameDetailsContext); const { game, repacks, gameColor } = useContext(gameDetailsContext);
@ -40,17 +46,16 @@ export function HeroPanel() {
return <HeroPanelPlaytime />; return <HeroPanelPlaytime />;
}; };
const backgroundColor = gameColor
? (new Color(gameColor).darken(0.6).toString() as string)
: "";
const showProgressBar = const showProgressBar =
(game?.status === "active" && game?.progress < 1) || (game?.status === "active" && game?.progress < 1) ||
game?.status === "paused"; game?.status === "paused";
return ( 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.content}>{getInfo()}</div>
<div className={styles.actions}> <div className={styles.actions}>
<HeroPanelActions /> <HeroPanelActions />

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components"; import { Button, Modal, TextField } from "@renderer/components";
import type { Game } from "@types"; import type { Game } from "@types";
import * as styles from "./game-options-modal.css"; 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 { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
import { useDownload } from "@renderer/hooks"; import { useDownload } from "@renderer/hooks";
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal"; import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
@ -21,7 +21,7 @@ export function GameOptionsModal({
}: GameOptionsModalProps) { }: GameOptionsModalProps) {
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
const { updateGame, openRepacksModal, selectGameExecutable } = const { updateGame, setShowRepacksModal, selectGameExecutable } =
useContext(gameDetailsContext); useContext(gameDetailsContext);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
@ -145,7 +145,7 @@ export function GameOptionsModal({
</div> </div>
<div className={styles.gameOptionRow}> <div className={styles.gameOptionRow}>
<Button <Button
onClick={openRepacksModal} onClick={() => setShowRepacksModal(true)}
theme="outline" theme="outline"
disabled={deleting || isGameDownloading} disabled={deleting || isGameDownloading}
> >

View File

@ -1,3 +1,3 @@
export * from "./installation-guides";
export * from "./repacks-modal"; export * from "./repacks-modal";
export * from "./download-settings-modal"; export * from "./download-settings-modal";
export * from "./game-options-modal";

View File

@ -1,4 +0,0 @@
export const DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY =
"dontShowOnlineFixInstructions";
export const DONT_SHOW_DODI_INSTRUCTIONS_KEY = "dontShowDodiInstructions";

View File

@ -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",
});

View File

@ -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>
);
}

View File

@ -1,3 +0,0 @@
export * from "./online-fix-installation-guide";
export * from "./dodi-installation-guide";
export * from "./constants";

View File

@ -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`,
});

View File

@ -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>
);
}

View File

@ -1,6 +1,7 @@
import { SPACING_UNIT } from "../../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../../theme.css";
export const deleteActionsButtonsCtn = style({ export const deleteActionsButtonsCtn = style({
display: "flex", display: "flex",
width: "100%", width: "100%",

View File

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

View File

@ -7,10 +7,10 @@ import type { GameRepack } from "@types";
import * as styles from "./repacks-modal.css"; 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 { format } from "date-fns";
import { DownloadSettingsModal } from "./download-settings-modal"; import { DownloadSettingsModal } from "./download-settings-modal";
import { gameDetailsContext } from "../game-details.context"; import { gameDetailsContext } from "@renderer/context";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
export interface RepacksModalProps { export interface RepacksModalProps {
@ -32,7 +32,7 @@ export function RepacksModal({
const [repack, setRepack] = useState<GameRepack | null>(null); const [repack, setRepack] = useState<GameRepack | null>(null);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false); const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const [infoHash, setInfoHash] = useState(""); const [infoHash, setInfoHash] = useState<string | null>(null);
const { repacks, game } = useContext(gameDetailsContext); const { repacks, game } = useContext(gameDetailsContext);
@ -40,7 +40,7 @@ export function RepacksModal({
const getInfoHash = useCallback(async () => { const getInfoHash = useCallback(async () => {
const torrent = await parseTorrent(game?.uri ?? ""); const torrent = await parseTorrent(game?.uri ?? "");
setInfoHash(torrent.infoHash ?? ""); if (torrent.infoHash) setInfoHash(torrent.infoHash);
}, [game]); }, [game]);
useEffect(() => { useEffect(() => {
@ -89,29 +89,35 @@ export function RepacksModal({
</div> </div>
<div className={styles.repacks}> <div className={styles.repacks}>
{filteredRepacks.map((repack) => ( {filteredRepacks.map((repack) => {
<Button const isLastDownloadedOption =
key={repack.id} infoHash !== null &&
theme="dark" repack.magnet.toLowerCase().includes(infoHash);
onClick={() => handleRepackClick(repack)}
className={styles.repackButton}
>
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
{repack.title}
</p>
{repack.magnet.toLowerCase().includes(infoHash) && ( return (
<Badge>{t("last_downloaded_option")}</Badge> <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" }}> {isLastDownloadedOption && (
{repack.fileSize} - {repack.repacker} -{" "} <Badge>{t("last_downloaded_option")}</Badge>
{repack.uploadDate )}
? format(repack.uploadDate, "dd/MM/yyyy")
: ""} <p style={{ fontSize: "12px" }}>
</p> {repack.fileSize} - {repack.repacker} -{" "}
</Button> {repack.uploadDate
))} ? format(repack.uploadDate, "dd/MM/yyyy")
: ""}
</p>
</Button>
);
})}
</div> </div>
</Modal> </Modal>
</> </>

View File

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

View File

@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import { Button } from "@renderer/components"; import { Button } from "@renderer/components";
import * as styles from "./sidebar.css"; import * as styles from "./sidebar.css";
import { gameDetailsContext } from "../game-details.context"; import { gameDetailsContext } from "@renderer/context";
export function Sidebar() { export function Sidebar() {
const [howLongToBeat, setHowLongToBeat] = useState<{ const [howLongToBeat, setHowLongToBeat] = useState<{

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import type { Steam250Game, CatalogueEntry } from "@types";
import starsAnimation from "@renderer/assets/lottie/stars.json"; import starsAnimation from "@renderer/assets/lottie/stars.json";
import * as styles from "./home.css"; import * as styles from "./home.css";
import { vars } from "../../theme.css"; import { vars } from "@renderer/theme.css";
import Lottie from "lottie-react"; import Lottie from "lottie-react";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";

View File

@ -9,13 +9,14 @@ import { debounce } from "lodash";
import { InboxIcon } from "@primer/octicons-react"; import { InboxIcon } from "@primer/octicons-react";
import { clearSearch } from "@renderer/features"; import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks"; import { useAppDispatch } from "@renderer/hooks";
import { vars } from "../../theme.css";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "./home.css"; import * as styles from "./home.css";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import { vars } from "@renderer/theme.css";
export function SearchResults() { export function SearchResults() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();

View File

@ -4,9 +4,10 @@ import { Trans, useTranslation } from "react-i18next";
import { Button, CheckboxField, Link, TextField } from "@renderer/components"; import { Button, CheckboxField, Link, TextField } from "@renderer/components";
import * as styles from "./settings-real-debrid.css"; import * as styles from "./settings-real-debrid.css";
import type { UserPreferences } from "@types"; import type { UserPreferences } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useAppSelector, useToast } from "@renderer/hooks"; import { useAppSelector, useToast } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken"; const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
export interface SettingsRealDebridProps { export interface SettingsRealDebridProps {

View File

@ -1,6 +1,7 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const container = style({ export const container = style({
padding: "24px", padding: "24px",
width: "100%", width: "100%",

View File

@ -1,6 +1,7 @@
import { Modal } from "@renderer/components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Modal } from "@renderer/components";
interface BinaryNotFoundModalProps { interface BinaryNotFoundModalProps {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;

View File

@ -1,8 +1,8 @@
import { createTheme } from "@vanilla-extract/css"; import { createGlobalTheme } from "@vanilla-extract/css";
export const SPACING_UNIT = 8; export const SPACING_UNIT = 8;
export const [themeClass, vars] = createTheme({ export const vars = createGlobalTheme(":root", {
color: { color: {
background: "#1c1c1c", background: "#1c1c1c",
darkBackground: "#151515", darkBackground: "#151515",