diff --git a/src/renderer/src/app.css.ts b/src/renderer/src/app.css.ts index cadab243c..652642401 100644 --- a/src/renderer/src/app.css.ts +++ b/src/renderer/src/app.css.ts @@ -7,6 +7,7 @@ globalStyle("*", { globalStyle("::-webkit-scrollbar", { width: "9px", + backgroundColor: vars.color.darkBackground, }); globalStyle("::-webkit-scrollbar-track", { diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 325e15066..67df3084c 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -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; } diff --git a/src/renderer/src/components/backdrop/backdrop.css.ts b/src/renderer/src/components/backdrop/backdrop.css.ts index 3b8cc4e2b..1c95717e1 100644 --- a/src/renderer/src/components/backdrop/backdrop.css.ts +++ b/src/renderer/src/components/backdrop/backdrop.css.ts @@ -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({ diff --git a/src/renderer/src/components/badge/badge.css.ts b/src/renderer/src/components/badge/badge.css.ts index 5cc674b5f..46cb7b870 100644 --- a/src/renderer/src/components/badge/badge.css.ts +++ b/src/renderer/src/components/badge/badge.css.ts @@ -1,4 +1,5 @@ import { style } from "@vanilla-extract/css"; + import { SPACING_UNIT } from "../../theme.css"; export const badge = style({ diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.css.ts b/src/renderer/src/components/bottom-panel/bottom-panel.css.ts index 22f71fe47..a1f5d1a80 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.css.ts +++ b/src/renderer/src/components/bottom-panel/bottom-panel.css.ts @@ -1,4 +1,5 @@ import { style } from "@vanilla-extract/css"; + import { SPACING_UNIT, vars } from "../../theme.css"; export const bottomPanel = style({ diff --git a/src/renderer/src/components/checkbox-field/checkbox-field.css.ts b/src/renderer/src/components/checkbox-field/checkbox-field.css.ts index 1aa7ee0ed..606b226ae 100644 --- a/src/renderer/src/components/checkbox-field/checkbox-field.css.ts +++ b/src/renderer/src/components/checkbox-field/checkbox-field.css.ts @@ -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", diff --git a/src/renderer/src/components/game-card/game-card.css.ts b/src/renderer/src/components/game-card/game-card.css.ts index b05b38b6d..c810130d6 100644 --- a/src/renderer/src/components/game-card/game-card.css.ts +++ b/src/renderer/src/components/game-card/game-card.css.ts @@ -1,4 +1,5 @@ import { style } from "@vanilla-extract/css"; + import { SPACING_UNIT, vars } from "../../theme.css"; export const card = style({ diff --git a/src/renderer/src/components/hero/hero.css.ts b/src/renderer/src/components/hero/hero.css.ts index fb8f68335..cdb36ee27 100644 --- a/src/renderer/src/components/hero/hero.css.ts +++ b/src/renderer/src/components/hero/hero.css.ts @@ -1,4 +1,5 @@ import { style } from "@vanilla-extract/css"; + import { SPACING_UNIT, vars } from "../../theme.css"; export const hero = style({ diff --git a/src/renderer/src/components/modal/modal.css.ts b/src/renderer/src/components/modal/modal.css.ts index 110f16f81..451540155 100644 --- a/src/renderer/src/components/modal/modal.css.ts +++ b/src/renderer/src/components/modal/modal.css.ts @@ -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({ diff --git a/src/renderer/src/components/modal/modal.tsx b/src/renderer/src/components/modal/modal.tsx index a71b781e6..eb2894de3 100644 --- a/src/renderer/src/components/modal/modal.tsx +++ b/src/renderer/src/components/modal/modal.tsx @@ -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(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; diff --git a/src/renderer/src/components/select-field/select-field.css.ts b/src/renderer/src/components/select-field/select-field.css.ts index 83a21c378..7acd4e986 100644 --- a/src/renderer/src/components/select-field/select-field.css.ts +++ b/src/renderer/src/components/select-field/select-field.css.ts @@ -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", diff --git a/src/renderer/src/components/sidebar/routes.tsx b/src/renderer/src/components/sidebar/routes.tsx index 8f54bac09..5535c2433 100644 --- a/src/renderer/src/components/sidebar/routes.tsx +++ b/src/renderer/src/components/sidebar/routes.tsx @@ -1,4 +1,5 @@ import { AppsIcon, GearIcon, HomeIcon } from "@primer/octicons-react"; + import { DownloadIcon } from "./download-icon"; export const routes = [ diff --git a/src/renderer/src/components/sidebar/sidebar.css.ts b/src/renderer/src/components/sidebar/sidebar.css.ts index 929241fa4..6677bf637 100644 --- a/src/renderer/src/components/sidebar/sidebar.css.ts +++ b/src/renderer/src/components/sidebar/sidebar.css.ts @@ -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", +}); diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index 3032359ef..897d6b18b 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -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,109 @@ export function Sidebar() { }; return ( - + ); } diff --git a/src/renderer/src/components/text-field/text-field.css.ts b/src/renderer/src/components/text-field/text-field.css.ts index 91dab4235..09f64498b 100644 --- a/src/renderer/src/components/text-field/text-field.css.ts +++ b/src/renderer/src/components/text-field/text-field.css.ts @@ -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`, diff --git a/src/renderer/src/pages/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx similarity index 63% rename from src/renderer/src/pages/game-details/game-details.context.tsx rename to src/renderer/src/context/game-details/game-details.context.tsx index e4889275b..ad32f9872 100644 --- a/src/renderer/src/pages/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -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>; - openRepacksModal: () => void; - openGameOptionsModal: () => void; - selectGameExecutable: () => Promise; - updateGame: () => Promise; -} +import { GameDetailsContext } from "./game-details.context.types"; export const gameDetailsContext = createContext({ game: null, @@ -45,11 +20,13 @@ export const gameDetailsContext = createContext({ 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 ( - <> - setShowRepacksModal(false)} - /> - - setShowInstructionsModal(null)} - /> - - setShowInstructionsModal(null)} - /> - - {game && ( - { - setShowGameOptionsModal(false); - }} - /> - )} - - {children} - + {children} ); } diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts new file mode 100644 index 000000000..36e55a793 --- /dev/null +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -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>; + selectGameExecutable: () => Promise; + updateGame: () => Promise; + setShowRepacksModal: React.Dispatch>; + setShowGameOptionsModal: React.Dispatch>; +} diff --git a/src/renderer/src/context/index.ts b/src/renderer/src/context/index.ts new file mode 100644 index 000000000..cecf58731 --- /dev/null +++ b/src/renderer/src/context/index.ts @@ -0,0 +1 @@ +export * from "./game-details/game-details.context"; diff --git a/src/renderer/src/pages/catalogue/catalogue.tsx b/src/renderer/src/pages/catalogue/catalogue.tsx index 6720b83f0..ee1f5395e 100644 --- a/src/renderer/src/pages/catalogue/catalogue.tsx +++ b/src/renderer/src/pages/catalogue/catalogue.tsx @@ -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(); diff --git a/src/renderer/src/pages/downloads/delete-game-modal.css.ts b/src/renderer/src/pages/downloads/delete-game-modal.css.ts index ef0ba179c..ca6c3888a 100644 --- a/src/renderer/src/pages/downloads/delete-game-modal.css.ts +++ b/src/renderer/src/pages/downloads/delete-game-modal.css.ts @@ -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%", diff --git a/src/renderer/src/pages/downloads/download-group.css.ts b/src/renderer/src/pages/downloads/download-group.css.ts index c928851be..d9bf263fd 100644 --- a/src/renderer/src/pages/downloads/download-group.css.ts +++ b/src/renderer/src/pages/downloads/download-group.css.ts @@ -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", diff --git a/src/renderer/src/pages/downloads/downloads.css.ts b/src/renderer/src/pages/downloads/downloads.css.ts index 06132e1e5..abb3c30bf 100644 --- a/src/renderer/src/pages/downloads/downloads.css.ts +++ b/src/renderer/src/pages/downloads/downloads.css.ts @@ -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`, diff --git a/src/renderer/src/pages/game-details/description-header/description-header.css.ts b/src/renderer/src/pages/game-details/description-header/description-header.css.ts new file mode 100644 index 000000000..45856f317 --- /dev/null +++ b/src/renderer/src/pages/game-details/description-header/description-header.css.ts @@ -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", +}); diff --git a/src/renderer/src/pages/game-details/description-header.tsx b/src/renderer/src/pages/game-details/description-header/description-header.tsx similarity index 84% rename from src/renderer/src/pages/game-details/description-header.tsx rename to src/renderer/src/pages/game-details/description-header/description-header.tsx index 9ac50ffaa..e42725349 100644 --- a/src/renderer/src/pages/game-details/description-header.tsx +++ b/src/renderer/src/pages/game-details/description-header/description-header.tsx @@ -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); diff --git a/src/renderer/src/pages/game-details/gallery-slider.css.ts b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.css.ts similarity index 97% rename from src/renderer/src/pages/game-details/gallery-slider.css.ts rename to src/renderer/src/pages/game-details/gallery-slider/gallery-slider.css.ts index f6995312b..5f8afcec6 100644 --- a/src/renderer/src/pages/game-details/gallery-slider.css.ts +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.css.ts @@ -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%", diff --git a/src/renderer/src/pages/game-details/gallery-slider.tsx b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx similarity index 98% rename from src/renderer/src/pages/game-details/gallery-slider.tsx rename to src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx index 93d77ebfe..47d177ac9 100644 --- a/src/renderer/src/pages/game-details/gallery-slider.tsx +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx @@ -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); diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx new file mode 100644 index 000000000..f6cb60e53 --- /dev/null +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -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(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 = (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 ( +
+ {game?.title} + +
+
+
+ +
+
+ {game?.title} +
+
+
+ + + +
+
+ + + +
+
+ + +
+
+
+ ); +} diff --git a/src/renderer/src/pages/game-details/game-details-skeleton.tsx b/src/renderer/src/pages/game-details/game-details-skeleton.tsx index b50f88d85..ab1c1d379 100644 --- a/src/renderer/src/pages/game-details/game-details-skeleton.tsx +++ b/src/renderer/src/pages/game-details/game-details-skeleton.tsx @@ -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() {
-
+
-
-
+
+
diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts index 7437960b3..fd8a48beb 100644 --- a/src/renderer/src/pages/game-details/game-details.css.ts +++ b/src/renderer/src/pages/game-details/game-details.css.ts @@ -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", diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index f9ab91a1e..b85fbc84f 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -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(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 ( - {({ game, shopDetails, isLoading, setGameColor }) => { - const handleHeroLoad = async () => { - const output = await average( - steamUrlBuilder.libraryHero(objectID!), - { - amount: 1, - format: "hex", - } - ); - - setGameColor(output as string); + {({ + 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, + }); + + await updateGame(); + setShowRepacksModal(false); + setShowGameOptionsModal(false); }; return ( @@ -77,47 +95,22 @@ export function GameDetails() { baseColor={vars.color.background} highlightColor="#444" > - {isLoading ? ( - - ) : ( -
-
- {game?.title} -
-
- {game?.title} -
-
-
- - - -
-
- - - -
-
- - -
-
+ {isLoading ? : } + + setShowRepacksModal(false)} + /> + + {game && ( + { + setShowGameOptionsModal(false); + }} + /> )} {fromRandomizer && ( diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index 6c35db3b5..4741668ea 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -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 = ( -
- - ); -} diff --git a/src/renderer/src/pages/game-details/modals/installation-guides/index.ts b/src/renderer/src/pages/game-details/modals/installation-guides/index.ts deleted file mode 100644 index ff5b129ed..000000000 --- a/src/renderer/src/pages/game-details/modals/installation-guides/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./online-fix-installation-guide"; -export * from "./dodi-installation-guide"; -export * from "./constants"; diff --git a/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.css.ts b/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.css.ts deleted file mode 100644 index b7665d7d4..000000000 --- a/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.css.ts +++ /dev/null @@ -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`, -}); diff --git a/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.tsx b/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.tsx deleted file mode 100644 index 3460eaec5..000000000 --- a/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.tsx +++ /dev/null @@ -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 ( - -
-

{t("online_fix_instruction")}

-
- - - -
- - setDontShowAgain(!dontShowAgain)} - checked={dontShowAgain} - /> - - -
-
- ); -} diff --git a/src/renderer/src/pages/game-details/modals/remove-from-library-modal.css.ts b/src/renderer/src/pages/game-details/modals/remove-from-library-modal.css.ts index eb676e57c..2c70717da 100644 --- a/src/renderer/src/pages/game-details/modals/remove-from-library-modal.css.ts +++ b/src/renderer/src/pages/game-details/modals/remove-from-library-modal.css.ts @@ -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%", diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts b/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts index 897d88f08..8d54e4028 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts @@ -1,4 +1,5 @@ import { style } from "@vanilla-extract/css"; + import { SPACING_UNIT, vars } from "../../../theme.css"; export const repacks = style({ diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index 6c7a30ebf..feae8e0ec 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -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(null); const [showSelectFolderModal, setShowSelectFolderModal] = useState(false); - const [infoHash, setInfoHash] = useState(""); + const [infoHash, setInfoHash] = useState(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({
- {filteredRepacks.map((repack) => ( - - ))} + {filteredRepacks.map((repack) => { + const isLastDownloadedOption = + infoHash !== null && + repack.magnet.toLowerCase().includes(infoHash); + + return ( + + ); + })}
diff --git a/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx b/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx index ab4e7a4c2..ffd148e53 100644 --- a/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx +++ b/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx @@ -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 = { diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 780f59644..6c963ee94 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -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<{ diff --git a/src/renderer/src/pages/home/catalogue-home.css.ts b/src/renderer/src/pages/home/catalogue-home.css.ts index c9f00cd78..8096bf974 100644 --- a/src/renderer/src/pages/home/catalogue-home.css.ts +++ b/src/renderer/src/pages/home/catalogue-home.css.ts @@ -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({ diff --git a/src/renderer/src/pages/home/home.css.ts b/src/renderer/src/pages/home/home.css.ts index b44da04af..7c6597ab8 100644 --- a/src/renderer/src/pages/home/home.css.ts +++ b/src/renderer/src/pages/home/home.css.ts @@ -1,4 +1,5 @@ import { style } from "@vanilla-extract/css"; + import { SPACING_UNIT, vars } from "../../theme.css"; export const homeHeader = style({ diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index 4ae2184e4..d8c109171 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -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"; diff --git a/src/renderer/src/pages/home/search-results.tsx b/src/renderer/src/pages/home/search-results.tsx index be461918f..30b3ea68c 100644 --- a/src/renderer/src/pages/home/search-results.tsx +++ b/src/renderer/src/pages/home/search-results.tsx @@ -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(); diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-real-debrid.tsx index d774de7ec..3dc4186d4 100644 --- a/src/renderer/src/pages/settings/settings-real-debrid.tsx +++ b/src/renderer/src/pages/settings/settings-real-debrid.tsx @@ -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 { diff --git a/src/renderer/src/pages/settings/settings.css.ts b/src/renderer/src/pages/settings/settings.css.ts index b73ba786c..9f607ecea 100644 --- a/src/renderer/src/pages/settings/settings.css.ts +++ b/src/renderer/src/pages/settings/settings.css.ts @@ -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%", diff --git a/src/renderer/src/pages/shared-modals/binary-not-found-modal.tsx b/src/renderer/src/pages/shared-modals/binary-not-found-modal.tsx index 06a216cbe..eeb0983af 100644 --- a/src/renderer/src/pages/shared-modals/binary-not-found-modal.tsx +++ b/src/renderer/src/pages/shared-modals/binary-not-found-modal.tsx @@ -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; diff --git a/src/renderer/src/theme.css.ts b/src/renderer/src/theme.css.ts index 1789e8a49..c748c1c7a 100644 --- a/src/renderer/src/theme.css.ts +++ b/src/renderer/src/theme.css.ts @@ -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",