feat: adding installation instructions

This commit is contained in:
Hydra 2024-05-02 22:22:23 +01:00
parent 5dd9ac9432
commit 193cc327cf
No known key found for this signature in database
45 changed files with 554 additions and 204 deletions

View File

@ -17,7 +17,10 @@
"downloading": "{{title}} ({{percentage}} - Downloading…)",
"filter": "Filter library",
"follow_us": "Follow us",
"home": "Home"
"home": "Home",
"discord": "Join our Discord",
"x": "Follow on X",
"github": "Contribute on GitHub"
},
"header": {
"search": "Search",
@ -82,8 +85,16 @@
"repacks_modal_description": "Choose the repack you want to download",
"downloads_path": "Downloads path",
"select_folder_hint": "To change the default folder, access the",
"settings": "Hydra settings",
"download_now": "Download now"
"settings": "Settings",
"download_now": "Download now",
"installation_instructions": "Installation Instructions",
"installation_instructions_description": "Additional steps are required to install this game",
"online_fix_instruction": "OnlineFix games requires a password to be extracted. When required, use the following password:",
"dodi_installation_instruction": "When you open DODI installer, press your keyboard up key <0 /> to start the installation process:",
"dont_show_it_again": "Don't show it again",
"copy_to_clipboard": "Copy",
"copied_to_clipboard": "Copied",
"got_it": "Got it"
},
"activation": {
"title": "Activate Hydra",
@ -143,5 +154,8 @@
"title": "Programs not installed",
"description": "Wine or Lutris executables were not found on your system",
"instructions": "Check the correct way to install any of them on your Linux distro so that the game can run normally"
},
"modal": {
"close": "Close button"
}
}

View File

@ -17,7 +17,10 @@
"downloading": "{{title}} ({{percentage}} - Baixando…)",
"filter": "Filtrar biblioteca",
"home": "Início",
"follow_us": "Acompanhe-nos"
"follow_us": "Acompanhe-nos",
"discord": "Entre no nosso Discord",
"x": "Siga-nos no X",
"github": "Contribua no GitHub"
},
"header": {
"search": "Buscar",
@ -79,7 +82,15 @@
"downloads_path": "Diretório do download",
"select_folder_hint": "Para trocar a pasta padrão, acesse as ",
"settings": "Configurações do Hydra",
"download_now": "Baixe agora"
"download_now": "Baixe agora",
"installation_instructions": "Instruções de Instalação",
"installation_instructions_description": "Passos adicionais são necessários para instalar esse jogos",
"online_fix_instruction": "Jogos OnlineFix precisam de uma senha para serem extraídos. Quando solicitado, utilize a seguinte senha:",
"dodi_installation_instruction": "Quando o instalador do DODI for aberto, pressione a seta para cima <0 /> do teclado para iniciar o processo de instalação:",
"dont_show_it_again": "Não mostrar novamente",
"copy_to_clipboard": "Copiar",
"copied_to_clipboard": "Copiado",
"got_it": "Entendi"
},
"activation": {
"title": "Ativação",
@ -143,5 +154,8 @@
"catalogue": {
"next_page": "Próxima página",
"previous_page": "Página anterior"
},
"modal": {
"close": "Botão de fechar"
}
}

View File

@ -1,5 +1,4 @@
import { app } from "electron";
import os from "node:os";
import path from "node:path";
export const repackersOn1337x = [

View File

@ -8,7 +8,7 @@ import { stateManager } from "@main/state-manager";
const { Index } = flexSearch;
const repacksIndex = new Index();
const steamGamesIndex = new Index({ tokenize: "reverse" });
const steamGamesIndex = new Index();
const repacks = stateManager.getValue("repacks");
const steamGames = stateManager.getValue("steamGames");

View File

@ -6,10 +6,10 @@
<title>Hydra</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://cdn2.steamgriddb.com;"
/>
</head>
<body style="background-color: #1c1c1">
<body style="background-color: #1c1c1c">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

View File

@ -26,6 +26,7 @@ globalStyle("body", {
overflow: "hidden",
userSelect: "none",
fontFamily: "'Fira Mono', monospace",
fontSize: vars.size.bodyFontSize,
background: vars.color.background,
color: vars.color.bodyText,
margin: "0",
@ -36,13 +37,16 @@ globalStyle("button", {
backgroundColor: "transparent",
border: "none",
fontFamily: "inherit",
fontSize: vars.size.bodyFontSize,
});
globalStyle("h1, h2, h3, h4, h5, h6, p", {
margin: 0,
});
globalStyle("p", {
lineHeight: "20px",
});
globalStyle("#root, main", {
display: "flex",
});
@ -103,5 +107,5 @@ export const titleBar = style({
padding: `0 ${SPACING_UNIT * 2}px`,
WebkitAppRegion: "drag",
zIndex: "2",
borderBottom: `1px solid ${vars.color.borderColor}`,
borderBottom: `1px solid ${vars.color.border}`,
} as ComplexStyleRule);

View File

@ -18,11 +18,16 @@ import {
clearSearch,
setUserPreferences,
setRepackersFriendlyNames,
toggleDraggingDisabled,
} from "@renderer/features";
document.body.classList.add(themeClass);
export function App({ children }: any) {
export interface AppProps {
children: React.ReactNode;
}
export function App({ children }: AppProps) {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary } = useLibrary();
@ -34,6 +39,9 @@ export function App({ children }: any) {
const location = useLocation();
const search = useAppSelector((state) => state.search.value);
const draggingDisabled = useAppSelector(
(state) => state.window.draggingDisabled
);
useEffect(() => {
Promise.all([
@ -93,6 +101,17 @@ export function App({ children }: any) {
if (contentRef.current) contentRef.current.scrollTop = 0;
}, [location.pathname, location.search]);
useEffect(() => {
new MutationObserver(() => {
const modal = document.body.querySelector("[role=modal]");
dispatch(toggleDraggingDisabled(Boolean(modal)));
}).observe(document.body, {
attributes: false,
childList: true,
});
}, [dispatch, draggingDisabled]);
return (
<>
{window.electron.platform === "win32" && (

View File

@ -3,13 +3,12 @@ import { SPACING_UNIT, vars } from "../../theme.css";
export const bottomPanel = style({
width: "100%",
borderTop: `solid 1px ${vars.color.borderColor}`,
borderTop: `solid 1px ${vars.color.border}`,
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`,
display: "flex",
alignItems: "center",
transition: "all ease 0.2s",
justifyContent: "space-between",
fontSize: vars.size.bodyFontSize,
zIndex: "1",
});

View File

@ -3,7 +3,7 @@ import { SPACING_UNIT, vars } from "../../theme.css";
const base = style({
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
backgroundColor: "#c0c1c7",
backgroundColor: vars.color.muted,
borderRadius: "8px",
border: "solid 1px transparent",
transition: "all ease 0.2s",
@ -35,8 +35,8 @@ export const button = styleVariants({
base,
{
backgroundColor: "transparent",
border: "solid 1px #c0c1c7",
color: "#c0c1c7",
border: `solid 1px ${vars.color.border}`,
color: vars.color.muted,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},

View File

@ -19,7 +19,7 @@ export const checkbox = style({
alignItems: "center",
position: "relative",
transition: "all ease 0.2s",
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
":hover": {
borderColor: "rgba(255, 255, 255, 0.5)",
},

View File

@ -10,7 +10,7 @@ export const card = recipe({
overflow: "hidden",
borderRadius: "4px",
transition: "all ease 0.2s",
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
cursor: "pointer",
zIndex: "1",
":active": {
@ -103,7 +103,7 @@ export const specifics = style({
export const specificsItem = style({
gap: `${SPACING_UNIT}px`,
display: "flex",
color: "#c0c1c7",
color: vars.color.muted,
fontSize: "12px",
alignItems: "flex-end",
});
@ -112,7 +112,7 @@ export const titleContainer = style({
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
color: "#c0c1c7",
color: vars.color.muted,
});
export const shopIcon = style({

View File

@ -29,8 +29,8 @@ export const header = recipe({
WebkitAppRegion: "drag",
width: "100%",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
color: "#c0c1c7",
borderBottom: `solid 1px ${vars.color.borderColor}`,
color: vars.color.muted,
borderBottom: `solid 1px ${vars.color.border}`,
backgroundColor: vars.color.darkBackground,
} as ComplexStyleRule,
variants: {
@ -55,7 +55,7 @@ export const search = recipe({
width: "200px",
alignItems: "center",
borderRadius: "8px",
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
height: "40px",
WebkitAppRegion: "no-drag",
} as ComplexStyleRule,
@ -83,7 +83,6 @@ export const searchInput = style({
color: "#DADBE1",
cursor: "default",
fontFamily: "inherit",
fontSize: vars.size.bodyFontSize,
textOverflow: "ellipsis",
":focus": {
cursor: "text",

View File

@ -11,7 +11,7 @@ export const hero = style({
overflow: "hidden",
boxShadow: "0px 0px 15px 0px #000000",
cursor: "pointer",
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
zIndex: "1",
});
@ -33,7 +33,7 @@ export const heroMedia = style({
export const backdrop = style({
width: "100%",
height: "100%",
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.6) 25%, transparent 100%)",
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%)",
position: "relative",
display: "flex",
overflow: "hidden",
@ -41,8 +41,7 @@ export const backdrop = style({
export const description = style({
maxWidth: "700px",
fontSize: vars.size.bodyFontSize,
color: "#c0c1c7",
color: vars.color.muted,
textAlign: "left",
fontFamily: "'Fira Sans', sans-serif",
lineHeight: "20px",

View File

@ -6,7 +6,7 @@ import { ShopDetails } from "@types";
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
import { useTranslation } from "react-i18next";
const FEATURED_GAME_ID = "253230";
const FEATURED_GAME_ID = "2420110";
export function Hero() {
const [featuredGameDetails, setFeaturedGameDetails] =
@ -36,7 +36,7 @@ export function Hero() {
>
<div className={styles.backdrop}>
<AsyncImage
src="https://cdn2.steamgriddb.com/hero/a6115ed32394915aac1e5502382eaaea.jpg"
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
alt={featuredGameDetails?.name}
className={styles.heroMedia}
/>

View File

@ -25,7 +25,7 @@ export const modal = recipe({
maxWidth: "600px",
color: vars.color.bodyText,
maxHeight: "100%",
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
display: "flex",
flexDirection: "column",
@ -50,13 +50,18 @@ export const modalHeader = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT * 2}px`,
borderBottom: `solid 1px ${vars.color.borderColor}`,
borderBottom: `solid 1px ${vars.color.border}`,
justifyContent: "space-between",
alignItems: "flex-start",
alignItems: "center",
});
export const closeModalButton = style({
cursor: "pointer",
transition: "all ease 0.2s",
alignSelf: "flex-start",
":hover": {
opacity: "0.75",
},
});
export const closeModalButtonIcon = style({

View File

@ -3,14 +3,14 @@ import { createPortal } from "react-dom";
import { XIcon } from "@primer/octicons-react";
import * as styles from "./modal.css";
import { useAppDispatch } from "@renderer/hooks";
import { toggleDragging } from "@renderer/features";
import { Backdrop } from "../backdrop/backdrop";
import { useTranslation } from "react-i18next";
export interface ModalProps {
visible: boolean;
title: string;
description: string;
description?: string;
onClose: () => void;
children: React.ReactNode;
}
@ -23,9 +23,10 @@ export function Modal({
children,
}: ModalProps) {
const [isClosing, setIsClosing] = useState(false);
const dispatch = useAppDispatch();
const modalContentRef = useRef<HTMLDivElement | null>(null);
const { t } = useTranslation("modal");
const handleCloseClick = useCallback(() => {
setIsClosing(true);
const zero = performance.now();
@ -82,10 +83,6 @@ export function Modal({
return () => {};
}, [handleCloseClick, visible]);
useEffect(() => {
dispatch(toggleDragging(visible));
}, [dispatch, visible]);
if (!visible) return null;
return createPortal(
@ -98,13 +95,14 @@ export function Modal({
<div className={styles.modalHeader}>
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
<h3>{title}</h3>
<p style={{ fontSize: 14 }}>{description}</p>
{description && <p>{description}</p>}
</div>
<button
type="button"
onClick={handleCloseClick}
className={styles.closeModalButton}
aria-label={t("close")}
>
<XIcon className={styles.closeModalButtonIcon} size={24} />
</button>

View File

@ -5,11 +5,11 @@ import { SPACING_UNIT, vars } from "../../theme.css";
export const sidebar = recipe({
base: {
backgroundColor: vars.color.darkBackground,
color: "#c0c1c7",
color: vars.color.muted,
flexDirection: "column",
display: "flex",
transition: "opacity ease 0.2s",
borderRight: `solid 1px ${vars.color.borderColor}`,
borderRight: `solid 1px ${vars.color.border}`,
position: "relative",
},
variants: {
@ -65,7 +65,7 @@ export const menuItem = recipe({
textWrap: "nowrap",
display: "flex",
opacity: "0.9",
color: "#DADBE1",
color: vars.color.muted,
":hover": {
opacity: "1",
},
@ -130,7 +130,7 @@ export const section = recipe({
variants: {
hasBorder: {
true: {
borderBottom: `solid 1px ${vars.color.borderColor}`,
borderBottom: `solid 1px ${vars.color.border}`,
},
},
},
@ -157,10 +157,10 @@ export const footerSocialsItem = style({
height: "16px",
display: "flex",
alignItems: "center",
transition: "all ease 0.15s",
transition: "all ease 0.2s",
cursor: "pointer",
":hover": {
opacity: 0.75,
cursor: "pointer",
opacity: "0.75",
},
});

View File

@ -16,21 +16,6 @@ import XLogo from "@renderer/assets/x-icon.svg?react";
import * as styles from "./sidebar.css";
const socials = [
{
url: "https://discord.gg/hydralauncher",
icon: <DiscordLogo />,
},
{
url: "https://twitter.com/hydralauncher",
icon: <XLogo />,
},
{
url: "https://github.com/hydralauncher/hydra",
icon: <MarkGithubIcon size={16} />,
},
];
const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250;
const SIDEBAR_MAX_WIDTH = 450;
@ -49,6 +34,24 @@ export function Sidebar() {
initialSidebarWidth ? Number(initialSidebarWidth) : SIDEBAR_INITIAL_WIDTH
);
const socials = [
{
url: "https://discord.gg/hydralauncher",
icon: <DiscordLogo />,
label: t("discord"),
},
{
url: "https://twitter.com/hydralauncher",
icon: <XLogo />,
label: t("x"),
},
{
url: "https://github.com/hydralauncher/hydra",
icon: <MarkGithubIcon size={16} />,
label: t("github"),
},
];
const location = useLocation();
const { game: gameDownloading, progress } = useDownload();
@ -243,6 +246,8 @@ export function Sidebar() {
key={item.url}
className={styles.footerSocialsItem}
onClick={() => window.electron.openExternal(item.url)}
title={item.label}
aria-label={item.label}
>
{item.icon}
</button>

View File

@ -9,7 +9,7 @@ export const textField = recipe({
width: "100%",
alignItems: "center",
borderRadius: "8px",
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
height: "40px",
minHeight: "40px",
},
@ -44,7 +44,6 @@ export const textFieldInput = style({
color: "#DADBE1",
cursor: "default",
fontFamily: "inherit",
fontSize: vars.size.bodyFontSize,
textOverflow: "ellipsis",
padding: `${SPACING_UNIT}px`,
":focus": {

View File

@ -9,11 +9,16 @@ export interface TextFieldProps
> {
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
label?: string;
textFieldProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
}
export function TextField({
theme = "primary",
label,
textFieldProps,
...props
}: TextFieldProps) {
const [isFocused, setIsFocused] = useState(false);
@ -27,7 +32,10 @@ export function TextField({
</label>
)}
<div className={styles.textField({ focused: isFocused, theme })}>
<div
className={styles.textField({ focused: isFocused, theme })}
{...textFieldProps}
>
<input
id={id}
type="text"

View File

@ -15,7 +15,7 @@ export const windowSlice = createSlice({
name: "window",
initialState,
reducers: {
toggleDragging: (state, action: PayloadAction<boolean>) => {
toggleDraggingDisabled: (state, action: PayloadAction<boolean>) => {
state.draggingDisabled = action.payload;
},
setHeaderTitle: (state, action: PayloadAction<string>) => {
@ -24,4 +24,4 @@ export const windowSlice = createSlice({
},
});
export const { toggleDragging, setHeaderTitle } = windowSlice.actions;
export const { toggleDraggingDisabled, setHeaderTitle } = windowSlice.actions;

View File

@ -21,5 +21,8 @@ export const getSteamLanguage = (language: string) => {
if (language.startsWith("pt")) return "brazilian";
if (language.startsWith("es")) return "spanish";
if (language.startsWith("fr")) return "french";
if (language.startsWith("ru")) return "russian";
if (language.startsWith("it")) return "italian";
if (language.startsWith("hu")) return "hungarian";
return "english";
};

View File

@ -6,7 +6,7 @@ import type { CatalogueEntry } from "@types";
import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks";
import { vars } from "../../theme.css";
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";
@ -67,12 +67,12 @@ export function Catalogue() {
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section
style={{
padding: `16px 32px`,
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 4}px`,
display: "flex",
width: "100%",
justifyContent: "space-between",
alignItems: "center",
borderBottom: `1px solid ${vars.color.borderColor}`,
borderBottom: `1px solid ${vars.color.border}`,
}}
>
<Button

View File

@ -31,7 +31,7 @@ export const downloadCover = style({
height: "auto",
objectFit: "cover",
objectPosition: "center",
borderRight: `solid 1px ${vars.color.borderColor}`,
borderRight: `solid 1px ${vars.color.border}`,
});
export const download = recipe({
@ -40,7 +40,7 @@ export const download = recipe({
backgroundColor: vars.color.background,
display: "flex",
borderRadius: "8px",
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
boxShadow: "0px 0px 15px 0px #000000",
transition: "all ease 0.2s",

View File

@ -15,43 +15,45 @@ export interface DescriptionHeaderProps {
}
export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
const [clipboardLock, setClipboardLock] = useState(false);
const [clipboardLocked, setClipboardLocked] = useState(false);
const { t, i18n } = useTranslation("game_details");
const { objectID, shop } = useParams();
useEffect(() => {
if (!gameDetails) return setClipboardLock(true);
setClipboardLock(false);
if (!gameDetails) return setClipboardLocked(true);
setClipboardLocked(false);
}, [gameDetails]);
const handleCopyToClipboard = () => {
setClipboardLock(true);
if (gameDetails) {
setClipboardLocked(true);
const searchParams = new URLSearchParams({
p: btoa(
JSON.stringify([
objectID,
shop,
encodeURIComponent(gameDetails?.name),
i18n.language,
])
),
});
const searchParams = new URLSearchParams({
p: btoa(
JSON.stringify([
objectID,
shop,
encodeURIComponent(gameDetails.name),
i18n.language,
])
),
});
navigator.clipboard.writeText(
OPEN_HYDRA_URL + `/?${searchParams.toString()}`
);
navigator.clipboard.writeText(
OPEN_HYDRA_URL + `/?${searchParams.toString()}`
);
const zero = performance.now();
const zero = performance.now();
requestAnimationFrame(function holdLock(time) {
if (time - zero <= 3000) {
requestAnimationFrame(holdLock);
} else {
setClipboardLock(false);
}
});
requestAnimationFrame(function holdLock(time) {
if (time - zero <= 3000) {
requestAnimationFrame(holdLock);
} else {
setClipboardLocked(false);
}
});
}
};
return (
@ -68,9 +70,9 @@ export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
<Button
theme="outline"
onClick={handleCopyToClipboard}
disabled={clipboardLock || !gameDetails}
disabled={clipboardLocked || !gameDetails}
>
{clipboardLock ? (
{clipboardLocked ? (
t("copied_link_to_clipboard")
) : (
<>

View File

@ -80,7 +80,7 @@ export const descriptionContent = style({
});
export const contentSidebar = style({
borderLeft: `solid 1px ${vars.color.borderColor};`,
borderLeft: `solid 1px ${vars.color.border};`,
width: "100%",
height: "100%",
"@media": {
@ -105,7 +105,6 @@ export const contentSidebarTitle = style({
display: "flex",
alignItems: "center",
backgroundColor: vars.color.background,
borderBottom: `solid 1px ${vars.color.borderColor}`,
});
export const requirementButtonContainer = style({
@ -114,7 +113,7 @@ export const requirementButtonContainer = style({
});
export const requirementButton = style({
border: `solid 1px ${vars.color.borderColor};`,
border: `solid 1px ${vars.color.border};`,
borderLeft: "none",
borderRight: "none",
borderRadius: "0",
@ -171,11 +170,11 @@ export const descriptionSkeleton = style({
export const descriptionHeader = style({
width: "100%",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
borderBottom: `solid 1px ${vars.color.borderColor}`,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: vars.color.background,
borderBottom: `solid 1px ${vars.color.border}`,
height: "72px",
});
@ -183,7 +182,6 @@ export const descriptionHeaderInfo = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
flexDirection: "column",
fontSize: vars.size.bodyFontSize,
});
export const howLongToBeatCategoriesList = style({
@ -201,16 +199,15 @@ export const howLongToBeatCategory = style({
backgroundColor: vars.color.background,
borderRadius: "8px",
padding: `8px 16px`,
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
});
export const howLongToBeatCategoryLabel = style({
fontSize: vars.size.bodyFontSize,
color: "#DADBE1",
color: vars.color.muted,
});
export const howLongToBeatCategorySkeleton = style({
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
borderRadius: "8px",
height: "76px",
});
@ -224,7 +221,7 @@ export const randomizerButton = style({
/* Scroll bar + spacing */
right: `${9 + SPACING_UNIT * 2}px`,
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px",
border: `solid 2px ${vars.color.borderColor}`,
border: `solid 2px ${vars.color.border}`,
backgroundColor: vars.color.background,
":hover": {
backgroundColor: vars.color.background,

View File

@ -5,6 +5,7 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import type {
Game,
GameRepack,
GameShop,
HowLongToBeatCategory,
ShopDetails,
@ -18,24 +19,30 @@ import { useAppDispatch, useDownload } from "@renderer/hooks";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import { vars } from "../../theme.css";
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-panel";
import { HeroPanel } from "./hero";
import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { RepacksModal } from "./repacks-modal";
import { OnlineFixInstallationGuide } from "./online-fix-installation-guide";
import { vars } from "../../theme.css";
import {
DODIInstallationGuide,
DONT_SHOW_DODI_INSTRUCTIONS_KEY,
DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY,
OnlineFixInstallationGuide,
} from "./installation-guides";
export function GameDetails() {
const { objectID, shop } = useParams();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
const [color, setColor] = useState("");
const [color, setColor] = useState({ dark: "", light: "" });
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
const [howLongToBeat, setHowLongToBeat] = useState<{
isLoading: boolean;
@ -44,6 +51,10 @@ export function GameDetails() {
const [game, setGame] = useState<Game | null>(null);
const [isGamePlaying, setIsGamePlaying] = useState(false);
const [showInstructionsModal, setShowInstructionsModal] = useState<
null | "onlinefix" | "DODI"
>(null);
const [activeRequirement, setActiveRequirement] =
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
@ -53,7 +64,6 @@ export function GameDetails() {
const { t, i18n } = useTranslation("game_details");
const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const dispatch = useAppDispatch();
@ -62,7 +72,8 @@ export function GameDetails() {
const handleImageSettled = useCallback((url: string) => {
average(url, { amount: 1, format: "hex" })
.then((color) => {
setColor(new Color(color).darken(0.6).toString() as string);
const darkColor = new Color(color).darken(0.6).toString() as string;
setColor({ light: color as string, dark: darkColor });
})
.catch(() => {});
}, []);
@ -113,7 +124,10 @@ export function GameDetails() {
useEffect(() => {
if (isGameDownloading)
setGame((prev) => ({ ...prev, status: gameDownloading?.status }));
setGame((prev) => {
if (prev === null || !gameDownloading?.status) return prev;
return { ...prev, status: gameDownloading?.status };
});
}, [isGameDownloading, gameDownloading?.status]);
useEffect(() => {
@ -135,12 +149,12 @@ export function GameDetails() {
}, [game?.id, isGamePlaying, getGame]);
const handleStartDownload = async (
repackId: number,
repack: GameRepack,
downloadPath: string
) => {
if (gameDetails) {
return startDownload(
repackId,
repack.id,
gameDetails.objectID,
gameDetails.name,
shop as GameShop,
@ -148,7 +162,18 @@ export function GameDetails() {
).then(() => {
getGame();
setShowRepacksModal(false);
setShowSelectFolderModal(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");
}
});
}
};
@ -168,19 +193,26 @@ export function GameDetails() {
return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
{/* <OnlineFixInstallationGuide /> */}
{gameDetails && (
<RepacksModal
visible={showRepacksModal}
gameDetails={gameDetails}
startDownload={handleStartDownload}
showSelectFolderModal={showSelectFolderModal}
setShowSelectFolderModal={setShowSelectFolderModal}
onClose={() => setShowRepacksModal(false)}
/>
)}
<OnlineFixInstallationGuide
visible={showInstructionsModal === "onlinefix"}
onClose={() => setShowInstructionsModal(null)}
/>
<DODIInstallationGuide
windowColor={color.light}
visible={showInstructionsModal === "DODI"}
onClose={() => setShowInstructionsModal(null)}
/>
{isLoading ? (
<GameDetailsSkeleton />
) : (
@ -204,7 +236,7 @@ export function GameDetails() {
<HeroPanel
game={game}
color={color}
color={color.dark}
gameDetails={gameDetails}
openRepacksModal={() => setShowRepacksModal(true)}
getGame={getGame}

View File

@ -0,0 +1,7 @@
import { style } from "@vanilla-extract/css";
import { vars } from "../../../theme.css";
export const heroPanelAction = style({
border: `solid 1px ${vars.color.muted}`,
});

View File

@ -6,6 +6,8 @@ import type { Game, ShopDetails } from "@types";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import * as styles from "./hero-panel-actions.css";
export interface HeroPanelActionsProps {
game: Game | null;
gameDetails: ShopDetails | null;
@ -55,6 +57,8 @@ export function HeroPanelActions({
if (filePaths && filePaths.length > 0) {
return filePaths[0];
}
return null;
});
};
@ -113,6 +117,7 @@ export function HeroPanelActions({
theme="outline"
disabled={!gameDetails || toggleLibraryGameDisabled}
onClick={toggleGameOnLibrary}
className={styles.heroPanelAction}
>
{game ? <NoEntryIcon /> : <PlusCircleIcon />}
{game ? t("remove_from_library") : t("add_to_library")}
@ -122,10 +127,18 @@ export function HeroPanelActions({
if (isGameDownloading) {
return (
<>
<Button onClick={() => pauseDownload(game.id)} theme="outline">
<Button
onClick={() => pauseDownload(game.id)}
theme="outline"
className={styles.heroPanelAction}
>
{t("pause")}
</Button>
<Button onClick={() => cancelDownload(game.id)} theme="outline">
<Button
onClick={() => cancelDownload(game.id)}
theme="outline"
className={styles.heroPanelAction}
>
{t("cancel")}
</Button>
</>
@ -135,12 +148,17 @@ export function HeroPanelActions({
if (game?.status === "paused") {
return (
<>
<Button onClick={() => resumeDownload(game.id)} theme="outline">
<Button
onClick={() => resumeDownload(game.id)}
theme="outline"
className={styles.heroPanelAction}
>
{t("resume")}
</Button>
<Button
onClick={() => cancelDownload(game.id).then(getGame)}
theme="outline"
className={styles.heroPanelAction}
>
{t("cancel")}
</Button>
@ -156,6 +174,7 @@ export function HeroPanelActions({
onClick={openGameInstaller}
theme="outline"
disabled={deleting || isGamePlaying}
className={styles.heroPanelAction}
>
{t("install")}
</Button>
@ -164,7 +183,12 @@ export function HeroPanelActions({
)}
{isGamePlaying ? (
<Button onClick={closeGame} theme="outline" disabled={deleting}>
<Button
onClick={closeGame}
theme="outline"
disabled={deleting}
className={styles.heroPanelAction}
>
{t("close")}
</Button>
) : (
@ -172,6 +196,7 @@ export function HeroPanelActions({
onClick={openGame}
theme="outline"
disabled={deleting || isGamePlaying}
className={styles.heroPanelAction}
>
{t("play")}
</Button>
@ -183,13 +208,19 @@ export function HeroPanelActions({
if (game?.status === "cancelled") {
return (
<>
<Button onClick={openRepacksModal} theme="outline" disabled={deleting}>
<Button
onClick={openRepacksModal}
theme="outline"
disabled={deleting}
className={styles.heroPanelAction}
>
{t("open_download_options")}
</Button>
<Button
onClick={() => removeGameFromLibrary(game.id).then(getGame)}
theme="outline"
disabled={deleting}
className={styles.heroPanelAction}
>
{t("remove_from_list")}
</Button>
@ -201,7 +232,11 @@ export function HeroPanelActions({
return (
<>
{toggleGameOnLibraryButton}
<Button onClick={openRepacksModal} theme="outline">
<Button
onClick={openRepacksModal}
theme="outline"
className={styles.heroPanelAction}
>
{t("open_download_options")}
</Button>
</>

View File

@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { SPACING_UNIT, vars } from "../../../theme.css";
export const panel = style({
width: "100%",
@ -9,7 +9,8 @@ export const panel = style({
alignItems: "center",
justifyContent: "space-between",
transition: "all ease 0.2s",
borderBottom: `solid 1px ${vars.color.borderColor}`,
borderBottom: `solid 1px ${vars.color.border}`,
color: "#8e919b",
boxShadow: "0px 0px 15px 0px #000000",
});
@ -17,7 +18,6 @@ export const content = style({
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
fontSize: vars.size.bodyFontSize,
});
export const actions = style({

View File

@ -6,12 +6,13 @@ import { useDownload } from "@renderer/hooks";
import type { Game, ShopDetails } from "@types";
import { formatDownloadProgress } from "@renderer/helpers";
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css";
import { useDate } from "@renderer/hooks/use-date";
import { formatBytes } from "@renderer/utils";
import { HeroPanelActions } from "./hero-panel-actions";
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css";
export interface HeroPanelProps {
game: Game | null;
gameDetails: ShopDetails | null;
@ -67,6 +68,8 @@ export function HeroPanel({
clearInterval(interval);
};
}
return () => {};
}, [game?.lastTimePlayed, updateLastTimePlayed]);
const finalDownloadSize = useMemo(() => {
@ -82,7 +85,7 @@ export function HeroPanel({
const getInfo = () => {
if (!gameDetails) return null;
if (isGameDeleting(game?.id)) {
if (isGameDeleting(game?.id ?? -1)) {
return <p>{t("deleting")}</p>;
}

View File

@ -0,0 +1 @@
export * from "./hero-panel";

View File

@ -1,6 +1,6 @@
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import type { HowLongToBeatCategory } from "@types";
import { useTranslation } from "react-i18next";
import type { HowLongToBeatCategory } from "@types";
import { vars } from "../../theme.css";
import * as styles from "./game-details.css";

View File

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

View File

@ -0,0 +1,31 @@
import { vars } from "../../../theme.css";
import { keyframes, style } from "@vanilla-extract/css";
export const slideIn = keyframes({
"0%": { transform: "translateY(0)" },
"40%": { transform: "translateY(0)" },
"70%": { transform: "translateY(-100%)" },
"100%": { transform: "translateY(-100%)" },
});
export const windowContainer = style({
width: "250px",
height: "150px",
alignSelf: "center",
borderRadius: "2px",
overflow: "hidden",
border: `solid 1px ${vars.color.border}`,
});
export const windowContent = style({
backgroundColor: vars.color.muted,
height: "90%",
animationName: slideIn,
animationDuration: "3s",
animationIterationCount: "infinite",
animationTimingFunction: "ease-out",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#1c1c1c",
});

View File

@ -0,0 +1,74 @@
import { 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";
export interface DODIInstallationGuideProps {
windowColor: string;
visible: boolean;
onClose: () => void;
}
export function DODIInstallationGuide({
windowColor,
visible,
onClose,
}: DODIInstallationGuideProps) {
const { t } = useTranslation("game_details");
const [dontShowAgain, setDontShowAgain] = useState(true);
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: 8 }}>
<Trans i18nKey="dodi_installation_instruction" ns="game_details">
<ArrowUpIcon size={16} />
</Trans>
</p>
<div
className={styles.windowContainer}
style={{ backgroundColor: windowColor }}
>
<div className={styles.windowContent}>
<ArrowUpIcon size={24} />
</div>
</div>
<CheckboxField
label={t("dont_show_it_again")}
onChange={() => setDontShowAgain(!dontShowAgain)}
checked={dontShowAgain}
/>
<Button style={{ alignSelf: "flex-end" }} onClick={handleClose}>
{t("got_it")}
</Button>
</div>
</Modal>
);
}

View File

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

View File

@ -0,0 +1,7 @@
import { SPACING_UNIT } from "../../../theme.css";
import { style } from "@vanilla-extract/css";
export const passwordField = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});

View File

@ -0,0 +1,103 @@
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(true);
const handleCopyToClipboard = () => {
setClipboardLocked(true);
navigator.clipboard.writeText(ONLINE_FIX_PASSWORD);
const zero = performance.now();
requestAnimationFrame(function holdLock(time) {
if (time - zero <= 3000) {
requestAnimationFrame(holdLock);
} else {
setClipboardLocked(false);
}
});
};
const handleClose = () => {
if (dontShowAgain) {
window.localStorage.setItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY, "1");
onClose();
}
};
return (
<Modal
title={t("installation_instructions")}
description={t("installation_instructions_description")}
onClose={handleClose}
visible={visible}
>
<div
style={{
display: "flex",
gap: SPACING_UNIT * 2,
flexDirection: "column",
}}
>
<p style={{ fontFamily: "Fira Sans" }}>{t("online_fix_instruction")}</p>
<div className={styles.passwordField}>
<TextField
value={ONLINE_FIX_PASSWORD}
readOnly
disabled
style={{ fontSize: 16 }}
textFieldProps={{ style: { height: 45 } }}
/>
<Button
style={{ alignSelf: "flex-end", height: 45 }}
theme="outline"
onClick={handleCopyToClipboard}
disabled={clipboardLocked}
>
{clipboardLocked ? (
t("copied_to_clipboard")
) : (
<>
<CopyIcon />
{t("copy_to_clipboard")}
</>
)}
</Button>
</div>
<CheckboxField
label={t("dont_show_it_again")}
onChange={() => setDontShowAgain(!dontShowAgain)}
checked={dontShowAgain}
/>
<Button style={{ alignSelf: "flex-end" }} onClick={handleClose}>
{t("got_it")}
</Button>
</div>
</Modal>
);
}

View File

@ -1,20 +0,0 @@
import { Button, CheckboxField, Modal, TextField } from "@renderer/components";
import { createPortal } from "react-dom";
export function OnlineFixInstallationGuide() {
return (
<Modal title="Extraction password" visible>
<form>
<p>
When asked for an extraction password for OnlineFix repacks, use the
following one:
</p>
<TextField value="online-fix.me" readOnly disabled />
<CheckboxField label="Don't show it again" />
<Button>Ok</Button>
</form>
</Modal>
);
}

View File

@ -14,22 +14,19 @@ import { SelectFolderModal } from "./select-folder-modal";
export interface RepacksModalProps {
visible: boolean;
gameDetails: ShopDetails;
showSelectFolderModal: boolean;
setShowSelectFolderModal: (value: boolean) => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
onClose: () => void;
}
export function RepacksModal({
visible,
gameDetails,
showSelectFolderModal,
setShowSelectFolderModal,
startDownload,
onClose,
}: RepacksModalProps) {
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack | null>(null);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const repackersFriendlyNames = useAppSelector(
(state) => state.repackersFriendlyNames.value
@ -57,12 +54,7 @@ export function RepacksModal({
};
return (
<Modal
visible={visible}
title={`${gameDetails.name} Repacks`}
description={t("repacks_modal_description")}
onClose={onClose}
>
<>
<SelectFolderModal
visible={showSelectFolderModal}
onClose={() => setShowSelectFolderModal(false)}
@ -70,26 +62,34 @@ export function RepacksModal({
startDownload={startDownload}
repack={repack}
/>
<div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}>
<TextField placeholder={t("filter")} onChange={handleFilter} />
</div>
<div className={styles.repacks}>
{filteredRepacks.map((repack) => (
<Button
key={repack.id}
theme="dark"
onClick={() => handleRepackClick(repack)}
className={styles.repackButton}
>
<p style={{ color: "#DADBE1" }}>{repack.title}</p>
<p style={{ fontSize: "12px" }}>
{repack.fileSize} - {repackersFriendlyNames[repack.repacker]} -{" "}
{format(repack.uploadDate, "dd/MM/yyyy")}
</p>
</Button>
))}
</div>
</Modal>
<Modal
visible={visible}
title={`${gameDetails.name} Repacks`}
description={t("repacks_modal_description")}
onClose={onClose}
>
<div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}>
<TextField placeholder={t("filter")} onChange={handleFilter} />
</div>
<div className={styles.repacks}>
{filteredRepacks.map((repack) => (
<Button
key={repack.id}
theme="dark"
onClick={() => handleRepackClick(repack)}
className={styles.repackButton}
>
<p style={{ color: "#DADBE1" }}>{repack.title}</p>
<p style={{ fontSize: "12px" }}>
{repack.fileSize} - {repackersFriendlyNames[repack.repacker]} -{" "}
{format(repack.uploadDate, "dd/MM/yyyy")}
</p>
</Button>
))}
</div>
</Modal>
</>
);
}

View File

@ -10,10 +10,18 @@ export const container = style({
export const downloadsPathField = style({
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
gap: `${SPACING_UNIT}px`,
});
export const hintText = style({
fontSize: "12px",
color: vars.color.bodyText,
});
export const settingsLink = style({
textDecoration: "none",
color: "#C0C1C7",
":hover": {
textDecoration: "underline",
},
});

View File

@ -7,12 +7,13 @@ import { formatBytes } from "@renderer/utils";
import { DiskSpace } from "check-disk-space";
import { Link } from "react-router-dom";
import * as styles from "./select-folder-modal.css";
import { DownloadIcon } from "@primer/octicons-react";
export interface SelectFolderModalProps {
visible: boolean;
gameDetails: ShopDetails;
onClose: () => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
repack: GameRepack | null;
}
@ -63,8 +64,10 @@ export function SelectFolderModal({
const handleStartClick = () => {
if (repack) {
setDownloadStarting(true);
startDownload(repack.id, selectedPath).finally(() => {
startDownload(repack, selectedPath).finally(() => {
setDownloadStarting(false);
onClose();
});
}
};
@ -98,17 +101,12 @@ export function SelectFolderModal({
</div>
<p className={styles.hintText}>
{t("select_folder_hint")}{" "}
<Link
to="/settings"
style={{
textDecoration: "none",
color: "#C0C1C7",
}}
>
<Link to="/settings" className={styles.settingsLink}>
{t("settings")}
</Link>
</p>
<Button onClick={handleStartClick} disabled={downloadStarting}>
<DownloadIcon />
{t("download_now")}
</Button>
</div>

View File

@ -12,7 +12,7 @@ export const content = style({
width: "100%",
height: "100%",
padding: `${SPACING_UNIT * 3}px`,
border: `solid 1px ${vars.color.borderColor}`,
border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px #000000",
borderRadius: "8px",
gap: `${SPACING_UNIT * 2}px`,
@ -22,5 +22,5 @@ export const content = style({
export const downloadsPathField = style({
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
gap: `${SPACING_UNIT}px`,
});

View File

@ -6,8 +6,9 @@ export const [themeClass, vars] = createTheme({
color: {
background: "#1c1c1c",
darkBackground: "#151515",
muted: "#c0c1c7",
bodyText: "#8e919b",
borderColor: "rgba(255, 255, 255, 0.1)",
border: "#424244",
},
opacity: {
disabled: "0.5",