mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge pull request #590 from hydralauncher/feature/adding-animation-to-game-details
Feature/adding animation to game details
This commit is contained in:
commit
55d1bfb34d
@ -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", {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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",
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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 = [
|
||||||
|
@ -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",
|
||||||
|
});
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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`,
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
||||||
|
|
||||||
|
export interface GameDetailsContext {
|
||||||
|
game: Game | null;
|
||||||
|
shopDetails: ShopDetails | null;
|
||||||
|
repacks: GameRepack[];
|
||||||
|
shop: GameShop;
|
||||||
|
gameTitle: string;
|
||||||
|
isGameRunning: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
objectID: string | undefined;
|
||||||
|
gameColor: string;
|
||||||
|
showRepacksModal: boolean;
|
||||||
|
showGameOptionsModal: boolean;
|
||||||
|
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
selectGameExecutable: () => Promise<string | null>;
|
||||||
|
updateGame: () => Promise<void>;
|
||||||
|
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setShowGameOptionsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
1
src/renderer/src/context/index.ts
Normal file
1
src/renderer/src/context/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./game-details/game-details.context";
|
@ -6,13 +6,14 @@ import type { CatalogueEntry } from "@types";
|
|||||||
|
|
||||||
import { clearSearch } from "@renderer/features";
|
import { 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();
|
||||||
|
|
||||||
|
@ -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%",
|
||||||
|
@ -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",
|
||||||
|
@ -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`,
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
|
|
||||||
|
export const descriptionHeader = style({
|
||||||
|
width: "100%",
|
||||||
|
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
height: "72px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const descriptionHeaderInfo = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
flexDirection: "column",
|
||||||
|
});
|
@ -1,8 +1,8 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { 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);
|
@ -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%",
|
@ -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);
|
116
src/renderer/src/pages/game-details/game-details-content.tsx
Normal file
116
src/renderer/src/pages/game-details/game-details-content.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { average } from "color.js";
|
||||||
|
import Color from "color";
|
||||||
|
|
||||||
|
import { steamUrlBuilder } from "@renderer/helpers";
|
||||||
|
|
||||||
|
import { HeroPanel } from "./hero";
|
||||||
|
import { DescriptionHeader } from "./description-header/description-header";
|
||||||
|
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||||
|
import { Sidebar } from "./sidebar/sidebar";
|
||||||
|
|
||||||
|
import * as styles from "./game-details.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
|
export function GameDetailsContent() {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
||||||
|
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const { objectID, shopDetails, game, gameColor, setGameColor } =
|
||||||
|
useContext(gameDetailsContext);
|
||||||
|
|
||||||
|
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
||||||
|
|
||||||
|
const handleHeroLoad = async () => {
|
||||||
|
const output = await average(steamUrlBuilder.libraryHero(objectID!), {
|
||||||
|
amount: 1,
|
||||||
|
format: "hex",
|
||||||
|
});
|
||||||
|
|
||||||
|
const backgroundColor = output
|
||||||
|
? (new Color(output).darken(0.7).toString() as string)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
setGameColor(backgroundColor);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBackdropOpacity(1);
|
||||||
|
}, [objectID]);
|
||||||
|
|
||||||
|
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
||||||
|
const scrollY = (event.target as HTMLDivElement).scrollTop;
|
||||||
|
const opacity = Math.max(0, 1 - scrollY / styles.HERO_HEIGHT);
|
||||||
|
|
||||||
|
if (scrollY >= styles.HERO_HEIGHT && !isHeaderStuck) {
|
||||||
|
setIsHeaderStuck(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollY <= styles.HERO_HEIGHT && isHeaderStuck) {
|
||||||
|
setIsHeaderStuck(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackdropOpacity(opacity);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.libraryHero(objectID!)}
|
||||||
|
className={styles.heroImage}
|
||||||
|
alt={game?.title}
|
||||||
|
onLoad={handleHeroLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section
|
||||||
|
ref={containerRef}
|
||||||
|
onScroll={onScroll}
|
||||||
|
className={styles.container}
|
||||||
|
>
|
||||||
|
<div className={styles.hero}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: gameColor,
|
||||||
|
flex: 1,
|
||||||
|
opacity: Math.min(1, 1 - backdropOpactiy),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.heroLogoBackdrop}
|
||||||
|
style={{ opacity: backdropOpactiy }}
|
||||||
|
>
|
||||||
|
<div className={styles.heroContent}>
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.logo(objectID!)}
|
||||||
|
style={{ width: 300, alignSelf: "flex-end" }}
|
||||||
|
alt={game?.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HeroPanel isHeaderStuck={isHeaderStuck} />
|
||||||
|
|
||||||
|
<div className={styles.descriptionContainer}>
|
||||||
|
<div className={styles.descriptionContent}>
|
||||||
|
<DescriptionHeader />
|
||||||
|
<GallerySlider />
|
||||||
|
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: shopDetails?.about_the_game ?? t("no_shop_details"),
|
||||||
|
}}
|
||||||
|
className={styles.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { Button } from "@renderer/components";
|
|||||||
|
|
||||||
import * as styles from "./game-details.css";
|
import * as 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>
|
||||||
|
@ -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",
|
||||||
|
@ -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 && (
|
||||||
|
@ -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}
|
||||||
|
@ -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() {
|
||||||
|
@ -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({
|
||||||
|
@ -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 />
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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";
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
export const DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY =
|
|
||||||
"dontShowOnlineFixInstructions";
|
|
||||||
|
|
||||||
export const DONT_SHOW_DODI_INSTRUCTIONS_KEY = "dontShowDodiInstructions";
|
|
@ -1,31 +0,0 @@
|
|||||||
import { vars } from "../../../../theme.css";
|
|
||||||
import { keyframes, style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
export const slideIn = keyframes({
|
|
||||||
"0%": { transform: "translateY(0)" },
|
|
||||||
"40%": { transform: "translateY(0)" },
|
|
||||||
"70%": { transform: "translateY(-100%)" },
|
|
||||||
"100%": { transform: "translateY(-100%)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const windowContainer = style({
|
|
||||||
width: "250px",
|
|
||||||
height: "150px",
|
|
||||||
alignSelf: "center",
|
|
||||||
borderRadius: "2px",
|
|
||||||
overflow: "hidden",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const windowContent = style({
|
|
||||||
backgroundColor: vars.color.muted,
|
|
||||||
height: "90%",
|
|
||||||
animationName: slideIn,
|
|
||||||
animationDuration: "3s",
|
|
||||||
animationIterationCount: "infinite",
|
|
||||||
animationTimingFunction: "ease-out",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: "#1c1c1c",
|
|
||||||
});
|
|
@ -1,78 +0,0 @@
|
|||||||
import { useContext, useState } from "react";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { Button, CheckboxField, Modal } from "@renderer/components";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
import * as styles from "./dodi-installation-guide.css";
|
|
||||||
import { ArrowUpIcon } from "@primer/octicons-react";
|
|
||||||
import { DONT_SHOW_DODI_INSTRUCTIONS_KEY } from "./constants";
|
|
||||||
import { gameDetailsContext } from "../../game-details.context";
|
|
||||||
|
|
||||||
export interface DODIInstallationGuideProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DODIInstallationGuide({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
}: DODIInstallationGuideProps) {
|
|
||||||
const { gameColor } = useContext(gameDetailsContext);
|
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
|
||||||
|
|
||||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
if (dontShowAgain) {
|
|
||||||
window.localStorage.setItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY, "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={t("installation_instructions")}
|
|
||||||
description={t("installation_instructions_description")}
|
|
||||||
onClose={handleClose}
|
|
||||||
visible={visible}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: SPACING_UNIT * 2,
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{ fontFamily: "Fira Sans", marginBottom: `${SPACING_UNIT}px` }}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="dodi_installation_instruction" ns="game_details">
|
|
||||||
<ArrowUpIcon size={16} />
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={styles.windowContainer}
|
|
||||||
style={{ backgroundColor: gameColor }}
|
|
||||||
>
|
|
||||||
<div className={styles.windowContent}>
|
|
||||||
<ArrowUpIcon size={24} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CheckboxField
|
|
||||||
label={t("dont_show_it_again")}
|
|
||||||
onChange={() => setDontShowAgain(!dontShowAgain)}
|
|
||||||
checked={dontShowAgain}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button style={{ alignSelf: "flex-end" }} onClick={handleClose}>
|
|
||||||
{t("got_it")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export * from "./online-fix-installation-guide";
|
|
||||||
export * from "./dodi-installation-guide";
|
|
||||||
export * from "./constants";
|
|
@ -1,7 +0,0 @@
|
|||||||
import { SPACING_UNIT } from "../../../../theme.css";
|
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
export const passwordField = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
@ -1,104 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { Button, CheckboxField, Modal, TextField } from "@renderer/components";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
import * as styles from "./online-fix-installation-guide.css";
|
|
||||||
import { CopyIcon } from "@primer/octicons-react";
|
|
||||||
import { DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY } from "./constants";
|
|
||||||
|
|
||||||
const ONLINE_FIX_PASSWORD = "online-fix.me";
|
|
||||||
|
|
||||||
export interface OnlineFixInstallationGuideProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OnlineFixInstallationGuide({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
}: OnlineFixInstallationGuideProps) {
|
|
||||||
const [clipboardLocked, setClipboardLocked] = useState(false);
|
|
||||||
const { t } = useTranslation("game_details");
|
|
||||||
|
|
||||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
|
||||||
|
|
||||||
const handleCopyToClipboard = () => {
|
|
||||||
setClipboardLocked(true);
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(ONLINE_FIX_PASSWORD);
|
|
||||||
|
|
||||||
const zero = performance.now();
|
|
||||||
|
|
||||||
requestAnimationFrame(function holdLock(time) {
|
|
||||||
if (time - zero <= 3000) {
|
|
||||||
requestAnimationFrame(holdLock);
|
|
||||||
} else {
|
|
||||||
setClipboardLocked(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
if (dontShowAgain) {
|
|
||||||
window.localStorage.setItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY, "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={t("installation_instructions")}
|
|
||||||
description={t("installation_instructions_description")}
|
|
||||||
onClose={handleClose}
|
|
||||||
visible={visible}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: SPACING_UNIT * 2,
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p style={{ fontFamily: "Fira Sans" }}>{t("online_fix_instruction")}</p>
|
|
||||||
<div className={styles.passwordField}>
|
|
||||||
<TextField
|
|
||||||
value={ONLINE_FIX_PASSWORD}
|
|
||||||
readOnly
|
|
||||||
disabled
|
|
||||||
style={{ fontSize: 16 }}
|
|
||||||
textFieldProps={{ style: { height: 45 } }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
style={{ alignSelf: "flex-end", height: 45 }}
|
|
||||||
theme="outline"
|
|
||||||
onClick={handleCopyToClipboard}
|
|
||||||
disabled={clipboardLocked}
|
|
||||||
>
|
|
||||||
{clipboardLocked ? (
|
|
||||||
t("copied_to_clipboard")
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CopyIcon />
|
|
||||||
{t("copy_to_clipboard")}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CheckboxField
|
|
||||||
label={t("dont_show_it_again")}
|
|
||||||
onChange={() => setDontShowAgain(!dontShowAgain)}
|
|
||||||
checked={dontShowAgain}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button style={{ alignSelf: "flex-end" }} onClick={handleClose}>
|
|
||||||
{t("got_it")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
import { SPACING_UNIT } from "../../../theme.css";
|
|
||||||
import { style } from "@vanilla-extract/css";
|
import { 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%",
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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> = {
|
||||||
|
@ -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<{
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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%",
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user