mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
feat: adding installation instructions
This commit is contained in:
parent
5dd9ac9432
commit
193cc327cf
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { app } from "electron";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export const repackersOn1337x = [
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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" && (
|
||||
|
@ -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",
|
||||
});
|
||||
|
||||
|
@ -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)",
|
||||
},
|
||||
|
@ -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)",
|
||||
},
|
||||
|
@ -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({
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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({
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
) : (
|
||||
<>
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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}`,
|
||||
});
|
@ -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>
|
||||
</>
|
@ -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({
|
@ -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>;
|
||||
}
|
||||
|
1
src/renderer/src/pages/game-details/hero/index.ts
Normal file
1
src/renderer/src/pages/game-details/hero/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./hero-panel";
|
@ -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";
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
export const DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY =
|
||||
"dontShowOnlineFixInstructions";
|
||||
export const DONT_SHOW_DODI_INSTRUCTIONS_KEY = "dontShowDodiInstructions";
|
@ -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",
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export * from "./online-fix-installation-guide";
|
||||
export * from "./dodi-installation-guide";
|
||||
export * from "./constants";
|
@ -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`,
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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`,
|
||||
});
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user