mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
chore: merge with dev
This commit is contained in:
commit
7c87e121bc
@ -14,12 +14,14 @@ import {
|
|||||||
|
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import "./sidebar.scss";
|
||||||
|
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
import cn from "classnames";
|
||||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
@ -168,9 +170,9 @@ export function Sidebar() {
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
className={styles.sidebar({
|
className={cn("sidebar", {
|
||||||
resizing: isResizing,
|
"sidebar--resizing": isResizing,
|
||||||
darwin: window.electron.platform === "darwin",
|
"sidebar--darwin": window.electron.platform === "darwin",
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
width: sidebarWidth,
|
width: sidebarWidth,
|
||||||
@ -183,19 +185,19 @@ export function Sidebar() {
|
|||||||
>
|
>
|
||||||
<SidebarProfile />
|
<SidebarProfile />
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className="sidebar__content">
|
||||||
<section className={styles.section}>
|
<section className="sidebar__section">
|
||||||
<ul className={styles.menu}>
|
<ul className="sidebar__menu">
|
||||||
{routes.map(({ nameKey, path, render }) => (
|
{routes.map(({ nameKey, path, render }) => (
|
||||||
<li
|
<li
|
||||||
key={nameKey}
|
key={nameKey}
|
||||||
className={styles.menuItem({
|
className={cn("sidebar__menu-item", {
|
||||||
active: location.pathname === path,
|
"sidebar__menu-item--active": location.pathname === path,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.menuItemButton}
|
className="sidebar__menu-item-button"
|
||||||
onClick={() => handleSidebarItemClick(path)}
|
onClick={() => handleSidebarItemClick(path)}
|
||||||
>
|
>
|
||||||
{render()}
|
{render()}
|
||||||
@ -206,8 +208,8 @@ export function Sidebar() {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className="sidebar__section">
|
||||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
<small className="sidebar__section-title">{t("my_library")}</small>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
ref={filterRef}
|
ref={filterRef}
|
||||||
@ -216,34 +218,35 @@ export function Sidebar() {
|
|||||||
theme="dark"
|
theme="dark"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ul className={styles.menu}>
|
<ul className="sidebar__menu">
|
||||||
{filteredLibrary.map((game) => (
|
{filteredLibrary.map((game) => (
|
||||||
<li
|
<li
|
||||||
key={`${game.shop}-${game.objectId}`}
|
key={game.id}
|
||||||
className={styles.menuItem({
|
className={cn("sidebar__menu-item", {
|
||||||
active:
|
"sidebar__menu-item--active":
|
||||||
location.pathname ===
|
location.pathname ===
|
||||||
`/game/${game.shop}/${game.objectId}`,
|
`/game/${game.shop}/${game.objectId}`,
|
||||||
muted: game.download?.status === "removed",
|
"sidebar__menu-item--muted":
|
||||||
|
game.download?.status === "removed",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.menuItemButton}
|
className="sidebar__menu-item-button"
|
||||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||||
>
|
>
|
||||||
{game.iconUrl ? (
|
{game.iconUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.gameIcon}
|
className="sidebar__game-icon"
|
||||||
src={game.iconUrl}
|
src={game.iconUrl}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SteamLogo className={styles.gameIcon} />
|
<SteamLogo className="sidebar__game-icon" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className={styles.menuItemButtonLabel}>
|
<span className="sidebar__menu-item-button-label">
|
||||||
{getGameTitle(game)}
|
{getGameTitle(game)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -257,10 +260,10 @@ export function Sidebar() {
|
|||||||
{hasActiveSubscription && (
|
{hasActiveSubscription && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.helpButton}
|
className="sidebar__help-button"
|
||||||
data-open-support-chat
|
data-open-support-chat
|
||||||
>
|
>
|
||||||
<div className={styles.helpButtonIcon}>
|
<div className="sidebar__help-button-icon">
|
||||||
<CommentDiscussionIcon size={14} />
|
<CommentDiscussionIcon size={14} />
|
||||||
</div>
|
</div>
|
||||||
<span>{t("need_help")}</span>
|
<span>{t("need_help")}</span>
|
||||||
@ -269,7 +272,7 @@ export function Sidebar() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.handle}
|
className="sidebar__handle"
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -1,97 +0,0 @@
|
|||||||
import { keyframes, style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
export const enter = keyframes({
|
|
||||||
"0%": {
|
|
||||||
opacity: 0,
|
|
||||||
transform: "translateY(100%)",
|
|
||||||
},
|
|
||||||
"100%": {
|
|
||||||
opacity: 1,
|
|
||||||
transform: "translateY(0)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const exit = keyframes({
|
|
||||||
"0%": {
|
|
||||||
opacity: 1,
|
|
||||||
transform: "translateY(0)",
|
|
||||||
},
|
|
||||||
"100%": {
|
|
||||||
opacity: 0,
|
|
||||||
transform: "translateY(100%)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const toast = recipe({
|
|
||||||
base: {
|
|
||||||
animationDuration: "0.15s",
|
|
||||||
animationTimingFunction: "ease-in-out",
|
|
||||||
maxWidth: "420px",
|
|
||||||
position: "absolute",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
right: "0",
|
|
||||||
bottom: "0",
|
|
||||||
overflow: "hidden",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
zIndex: vars.zIndex.toast,
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
closing: {
|
|
||||||
true: {
|
|
||||||
animationName: exit,
|
|
||||||
transform: "translateY(100%)",
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
animationName: enter,
|
|
||||||
transform: "translateY(0)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const toastContent = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const progress = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "3px",
|
|
||||||
"::-webkit-progress-bar": {
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
},
|
|
||||||
"::-webkit-progress-value": {
|
|
||||||
backgroundColor: vars.color.muted,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const closeButton = style({
|
|
||||||
color: vars.color.body,
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all ease 0.15s",
|
|
||||||
":hover": {
|
|
||||||
color: vars.color.muted,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const successIcon = style({
|
|
||||||
color: vars.color.success,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const errorIcon = style({
|
|
||||||
color: vars.color.danger,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const warningIcon = style({
|
|
||||||
color: vars.color.warning,
|
|
||||||
});
|
|
@ -6,12 +6,13 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./toast.css";
|
import "./toast.scss";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import cn from "classnames";
|
||||||
|
|
||||||
export interface ToastProps {
|
export interface ToastProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
message: string;
|
title: string;
|
||||||
|
message?: string;
|
||||||
type: "success" | "error" | "warning";
|
type: "success" | "error" | "warning";
|
||||||
duration?: number;
|
duration?: number;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -21,9 +22,10 @@ const INITIAL_PROGRESS = 100;
|
|||||||
|
|
||||||
export function Toast({
|
export function Toast({
|
||||||
visible,
|
visible,
|
||||||
|
title,
|
||||||
message,
|
message,
|
||||||
type,
|
type,
|
||||||
duration = 5000,
|
duration = 2500,
|
||||||
onClose,
|
onClose,
|
||||||
}: Readonly<ToastProps>) {
|
}: Readonly<ToastProps>) {
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
@ -79,12 +81,16 @@ export function Toast({
|
|||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.toast({ closing: isClosing })}>
|
<div
|
||||||
<div className={styles.toastContent}>
|
className={cn("toast", {
|
||||||
|
"toast--closing": isClosing,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="toast__content">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `8px`,
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -93,24 +99,26 @@ export function Toast({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `8px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === "success" && (
|
{type === "success" && (
|
||||||
<CheckCircleFillIcon className={styles.successIcon} />
|
<CheckCircleFillIcon className="toast__icon--success" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "error" && (
|
{type === "error" && (
|
||||||
<XCircleFillIcon className={styles.errorIcon} />
|
<XCircleFillIcon className="toast__icon--error" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "warning" && <AlertIcon className={styles.warningIcon} />}
|
{type === "warning" && (
|
||||||
|
<AlertIcon className="toast__icon--warning" />
|
||||||
|
)}
|
||||||
|
|
||||||
<span style={{ fontWeight: "bold", flex: 1 }}>{message}</span>
|
<span style={{ fontWeight: "bold", flex: 1 }}>{title}</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.closeButton}
|
className="toast__close-button"
|
||||||
onClick={startAnimateClosing}
|
onClick={startAnimateClosing}
|
||||||
aria-label="Close toast"
|
aria-label="Close toast"
|
||||||
>
|
>
|
||||||
@ -118,14 +126,11 @@ export function Toast({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
{message && <p>{message}</p>}
|
||||||
This is a really really long message that should wrap to the next
|
|
||||||
line
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<progress className={styles.progress} value={progress} max={100} />
|
<progress className="toast__progress" value={progress} max={100} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,38 @@
|
|||||||
import { useDate } from "@renderer/hooks";
|
import { useDate } from "@renderer/hooks";
|
||||||
import type { UserAchievement } from "@types";
|
import type { UserAchievement } from "@types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./achievements.css";
|
import "./achievements.scss";
|
||||||
import { EyeClosedIcon } from "@primer/octicons-react";
|
import { EyeClosedIcon } from "@primer/octicons-react";
|
||||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
interface AchievementListProps {
|
interface AchievementListProps {
|
||||||
achievements: UserAchievement[];
|
achievements: UserAchievement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AchievementList({ achievements }: AchievementListProps) {
|
export function AchievementList({
|
||||||
|
achievements,
|
||||||
|
}: Readonly<AchievementListProps>) {
|
||||||
const { t } = useTranslation("achievement");
|
const { t } = useTranslation("achievement");
|
||||||
const { showHydraCloudModal } = useSubscription();
|
const { showHydraCloudModal } = useSubscription();
|
||||||
const { formatDateTime } = useDate();
|
const { formatDateTime } = useDate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={styles.list}>
|
<ul className="achievements__list">
|
||||||
{achievements.map((achievement) => (
|
{achievements.map((achievement) => (
|
||||||
<li
|
<li key={achievement.name} className="achievements__item">
|
||||||
key={achievement.name}
|
|
||||||
className={styles.listItem}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
className={styles.listItemImage({
|
className={`achievements__item-image ${!achievement.unlocked ? "achievements__item-image--locked" : ""}`}
|
||||||
unlocked: achievement.unlocked,
|
|
||||||
})}
|
|
||||||
src={achievement.icon}
|
src={achievement.icon}
|
||||||
alt={achievement.displayName}
|
alt={achievement.displayName}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div className="achievements__item-content">
|
||||||
<h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
<h4 className="achievements__item-title">
|
||||||
{achievement.hidden && (
|
{achievement.hidden && (
|
||||||
<span
|
<span
|
||||||
style={{ display: "flex" }}
|
className="achievements__item-hidden-icon"
|
||||||
title={t("hidden_achievement_tooltip")}
|
title={t("hidden_achievement_tooltip")}
|
||||||
>
|
>
|
||||||
<EyeClosedIcon size={12} />
|
<EyeClosedIcon size={12} />
|
||||||
@ -47,48 +42,36 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
|||||||
</h4>
|
</h4>
|
||||||
<p>{achievement.description}</p>
|
<p>{achievement.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
style={{
|
<div className="achievements__item-meta">
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "8px",
|
|
||||||
alignItems: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{achievement.points != undefined ? (
|
{achievement.points != undefined ? (
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", alignItems: "center", gap: "4px" }}
|
className="achievements__item-points"
|
||||||
title={t("achievement_earn_points", {
|
title={t("achievement_earn_points", {
|
||||||
points: achievement.points,
|
points: achievement.points,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<HydraIcon width={20} height={20} />
|
<HydraIcon className="achievements__item-points-icon" />
|
||||||
<p style={{ fontSize: "1.1em" }}>{achievement.points}</p>
|
<p className="achievements__item-points-value">
|
||||||
|
{achievement.points}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => showHydraCloudModal("achievements")}
|
onClick={() => showHydraCloudModal("achievements")}
|
||||||
style={{
|
className="achievements__item-points achievements__item-points--locked"
|
||||||
display: "flex",
|
title={t("achievement_earn_points", { points: "???" })}
|
||||||
alignItems: "center",
|
|
||||||
gap: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
color: vars.color.warning,
|
|
||||||
}}
|
|
||||||
title={t("achievement_earn_points", {
|
|
||||||
points: "???",
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<HydraIcon width={20} height={20} />
|
<HydraIcon className="achievements__item-points-icon" />
|
||||||
<p style={{ fontSize: "1.1em" }}>???</p>
|
<p className="achievements__item-points-value">???</p>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{achievement.unlockTime != null && (
|
{achievement.unlockTime != null && (
|
||||||
<div
|
<div
|
||||||
|
className="achievements__item-unlock-time"
|
||||||
title={t("unlocked_at", {
|
title={t("unlocked_at", {
|
||||||
date: formatDateTime(achievement.unlockTime),
|
date: formatDateTime(achievement.unlockTime),
|
||||||
})}
|
})}
|
||||||
style={{ whiteSpace: "nowrap", gap: "4px", display: "flex" }}
|
|
||||||
>
|
>
|
||||||
<small>{formatDateTime(achievement.unlockTime)}</small>
|
<small>{formatDateTime(achievement.unlockTime)}</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,9 +12,8 @@ import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
|||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
import { useAppSelector, useDownload } from "@renderer/hooks";
|
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./download-group.css";
|
import "./download-group.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -260,44 +259,26 @@ export function DownloadGroup({
|
|||||||
if (!library.length) return null;
|
if (!library.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.downloadGroup}>
|
<div className="download-group">
|
||||||
<div
|
<div className="download-group__header">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
|
<div className="download-group__header-divider" />
|
||||||
<div
|
<h3 className="download-group__header-count">{library.length}</h3>
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: vars.color.border,
|
|
||||||
height: "1px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<h3 style={{ fontWeight: "400" }}>{library.length}</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className={styles.downloads}>
|
<ul className="download-group__downloads">
|
||||||
{library.map((game) => {
|
{library.map((game) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li key={game.id} className="download-group__item">
|
||||||
key={game.id}
|
<div className="download-group__cover">
|
||||||
className={styles.download}
|
<div className="download-group__cover-backdrop">
|
||||||
style={{ position: "relative" }}
|
|
||||||
>
|
|
||||||
<div className={styles.downloadCover}>
|
|
||||||
<div className={styles.downloadCoverBackdrop}>
|
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectId)}
|
src={steamUrlBuilder.library(game.objectId)}
|
||||||
className={styles.downloadCoverImage}
|
className="download-group__cover-image"
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.downloadCoverContent}>
|
<div className="download-group__cover-content">
|
||||||
<Badge>
|
<Badge>
|
||||||
{
|
{
|
||||||
DOWNLOADER_NAME[
|
DOWNLOADER_NAME[
|
||||||
@ -308,12 +289,12 @@ export function DownloadGroup({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.downloadRightContent}>
|
<div className="download-group__right-content">
|
||||||
<div className={styles.downloadDetails}>
|
<div className="download-group__details">
|
||||||
<div className={styles.downloadTitleWrapper}>
|
<div className="download-group__title-wrapper">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.downloadTitle}
|
className="download-group__title"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(
|
navigate(
|
||||||
buildGameDetailsPath({
|
buildGameDetailsPath({
|
||||||
@ -337,15 +318,7 @@ export function DownloadGroup({
|
|||||||
sideOffset={-75}
|
sideOffset={-75}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
style={{
|
className="download-group__menu-button"
|
||||||
position: "absolute",
|
|
||||||
top: "12px",
|
|
||||||
right: "12px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
border: "none",
|
|
||||||
padding: "8px",
|
|
||||||
minHeight: "unset",
|
|
||||||
}}
|
|
||||||
theme="outline"
|
theme="outline"
|
||||||
>
|
>
|
||||||
<ThreeBarsIcon />
|
<ThreeBarsIcon />
|
||||||
|
@ -1,24 +1,18 @@
|
|||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./hero-panel.css";
|
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
import { useDate, useDownload, useFormat } from "@renderer/hooks";
|
import { useDate, useDownload, useFormat } from "@renderer/hooks";
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
|
import "./hero-panel-playtime.scss";
|
||||||
|
|
||||||
export function HeroPanelPlaytime() {
|
export function HeroPanelPlaytime() {
|
||||||
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
||||||
|
|
||||||
const { game, isGameRunning } = useContext(gameDetailsContext);
|
const { game, isGameRunning } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const { progress, lastPacket } = useDownload();
|
const { progress, lastPacket } = useDownload();
|
||||||
|
|
||||||
const { formatDistance } = useDate();
|
const { formatDistance } = useDate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -56,8 +50,8 @@ export function HeroPanelPlaytime() {
|
|||||||
game.download?.status === "active" && lastPacket?.gameId === game.id;
|
game.download?.status === "active" && lastPacket?.gameId === game.id;
|
||||||
|
|
||||||
const downloadInProgressInfo = (
|
const downloadInProgressInfo = (
|
||||||
<div className={styles.downloadDetailsRow}>
|
<div className="hero-panel-playtime__download-details">
|
||||||
<Link to="/downloads" className={styles.downloadsLink}>
|
<Link to="/downloads" className="hero-panel-playtime__downloads-link">
|
||||||
{game.download?.status === "active"
|
{game.download?.status === "active"
|
||||||
? t("download_in_progress")
|
? t("download_in_progress")
|
||||||
: t("download_paused")}
|
: t("download_paused")}
|
||||||
@ -84,7 +78,6 @@ export function HeroPanelPlaytime() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>{t("playing_now")}</p>
|
<p>{t("playing_now")}</p>
|
||||||
|
|
||||||
{hasDownload && downloadInProgressInfo}
|
{hasDownload && downloadInProgressInfo}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useDate, useDownload } from "@renderer/hooks";
|
import { useDate, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import { HeroPanelActions } from "./hero-panel-actions";
|
import { HeroPanelActions } from "./hero-panel-actions";
|
||||||
import * as styles from "./hero-panel.css";
|
|
||||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||||
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
import "./hero-panel.scss";
|
||||||
|
|
||||||
export interface HeroPanelProps {
|
export interface HeroPanelProps {
|
||||||
isHeaderStuck: boolean;
|
isHeaderStuck: boolean;
|
||||||
@ -54,30 +54,28 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
|||||||
game?.download?.status === "paused";
|
game?.download?.status === "paused";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
style={{ backgroundColor: gameColor }}
|
||||||
style={{ backgroundColor: gameColor }}
|
className={`hero-panel ${isHeaderStuck ? "hero-panel--stuck" : ""}`}
|
||||||
className={styles.panel({ stuck: isHeaderStuck })}
|
>
|
||||||
>
|
<div className="hero-panel__content">{getInfo()}</div>
|
||||||
<div className={styles.content}>{getInfo()}</div>
|
<div className="hero-panel__actions">
|
||||||
<div className={styles.actions}>
|
<HeroPanelActions />
|
||||||
<HeroPanelActions />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showProgressBar && (
|
|
||||||
<progress
|
|
||||||
max={1}
|
|
||||||
value={
|
|
||||||
isGameDownloading
|
|
||||||
? lastPacket?.progress
|
|
||||||
: game?.download?.progress
|
|
||||||
}
|
|
||||||
className={styles.progressBar({
|
|
||||||
disabled: game?.download?.status === "paused",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{showProgressBar && (
|
||||||
|
<progress
|
||||||
|
max={1}
|
||||||
|
value={
|
||||||
|
isGameDownloading ? lastPacket?.progress : game?.download?.progress
|
||||||
|
}
|
||||||
|
className={`hero-panel__progress-bar ${
|
||||||
|
game?.download?.status === "paused"
|
||||||
|
? "hero-panel__progress-bar--disabled"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { useContext, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
import * as styles from "./game-options-modal.css";
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||||
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
||||||
@ -10,6 +9,7 @@ import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
|||||||
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
||||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
|
import "./game-options-modal.scss";
|
||||||
|
|
||||||
export interface GameOptionsModalProps {
|
export interface GameOptionsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -21,7 +21,7 @@ export function GameOptionsModal({
|
|||||||
visible,
|
visible,
|
||||||
game,
|
game,
|
||||||
onClose,
|
onClose,
|
||||||
}: GameOptionsModalProps) {
|
}: Readonly<GameOptionsModalProps>) {
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { showSuccessToast, showErrorToast } = useToast();
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
@ -192,14 +192,12 @@ export function GameOptionsModal({
|
|||||||
onClose={() => setShowDeleteModal(false)}
|
onClose={() => setShowDeleteModal(false)}
|
||||||
deleteGame={handleDeleteGame}
|
deleteGame={handleDeleteGame}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RemoveGameFromLibraryModal
|
<RemoveGameFromLibraryModal
|
||||||
visible={showRemoveGameModal}
|
visible={showRemoveGameModal}
|
||||||
onClose={() => setShowRemoveGameModal(false)}
|
onClose={() => setShowRemoveGameModal(false)}
|
||||||
removeGameFromLibrary={handleRemoveGameFromLibrary}
|
removeGameFromLibrary={handleRemoveGameFromLibrary}
|
||||||
game={game}
|
game={game}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ResetAchievementsModal
|
<ResetAchievementsModal
|
||||||
visible={showResetAchievementsModal}
|
visible={showResetAchievementsModal}
|
||||||
onClose={() => setShowResetAchievementsModal(false)}
|
onClose={() => setShowResetAchievementsModal(false)}
|
||||||
@ -213,59 +211,66 @@ export function GameOptionsModal({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
large={true}
|
large={true}
|
||||||
>
|
>
|
||||||
<div className={styles.optionsContainer}>
|
<div className="game-options-modal__container">
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__section">
|
||||||
<h2>{t("executable_section_title")}</h2>
|
<div className="game-options-modal__header">
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h2>{t("executable_section_title")}</h2>
|
||||||
{t("executable_section_description")}
|
<h4 className="game-options-modal__header-description">
|
||||||
</h4>
|
{t("executable_section_description")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="game-options-modal__executable-field">
|
||||||
|
<TextField
|
||||||
|
value={game.executablePath || ""}
|
||||||
|
readOnly
|
||||||
|
theme="dark"
|
||||||
|
disabled
|
||||||
|
placeholder={t("no_executable_selected")}
|
||||||
|
rightContent={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleChangeExecutableLocation}
|
||||||
|
>
|
||||||
|
<FileIcon />
|
||||||
|
{t("select_executable")}
|
||||||
|
</Button>
|
||||||
|
{game.executablePath && (
|
||||||
|
<Button
|
||||||
|
onClick={handleClearExecutablePath}
|
||||||
|
theme="outline"
|
||||||
|
>
|
||||||
|
{t("clear")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{game.executablePath && (
|
||||||
|
<div className="game-options-modal__executable-field-buttons">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleOpenGameExecutablePath}
|
||||||
|
>
|
||||||
|
{t("open_folder")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreateShortcut} theme="outline">
|
||||||
|
{t("create_shortcut")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextField
|
|
||||||
value={game.executablePath || ""}
|
|
||||||
readOnly
|
|
||||||
theme="dark"
|
|
||||||
disabled
|
|
||||||
placeholder={t("no_executable_selected")}
|
|
||||||
rightContent={
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="outline"
|
|
||||||
onClick={handleChangeExecutableLocation}
|
|
||||||
>
|
|
||||||
<FileIcon />
|
|
||||||
{t("select_executable")}
|
|
||||||
</Button>
|
|
||||||
{game.executablePath && (
|
|
||||||
<Button onClick={handleClearExecutablePath} theme="outline">
|
|
||||||
{t("clear")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{game.executablePath && (
|
|
||||||
<div className={styles.gameOptionRow}>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="outline"
|
|
||||||
onClick={handleOpenGameExecutablePath}
|
|
||||||
>
|
|
||||||
{t("open_folder")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleCreateShortcut} theme="outline">
|
|
||||||
{t("create_shortcut")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{shouldShowWinePrefixConfiguration && (
|
{shouldShowWinePrefixConfiguration && (
|
||||||
<div className={styles.optionsContainer}>
|
<div className="game-options-modal__wine-prefix">
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__header">
|
||||||
<h2>{t("wine_prefix")}</h2>
|
<h2>{t("wine_prefix")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className="game-options-modal__header-description">
|
||||||
{t("wine_prefix_description")}
|
{t("wine_prefix_description")}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -300,11 +305,13 @@ export function GameOptionsModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowLaunchOptionsConfiguration && (
|
{shouldShowLaunchOptionsConfiguration && (
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__launch-options">
|
||||||
<h2>{t("launch_options")}</h2>
|
<div className="game-options-modal__header">
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h2>{t("launch_options")}</h2>
|
||||||
{t("launch_options_description")}
|
<h4 className="game-options-modal__header-description">
|
||||||
</h4>
|
{t("launch_options_description")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
value={launchOptions}
|
value={launchOptions}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
@ -321,72 +328,76 @@ export function GameOptionsModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__downloads">
|
||||||
<h2>{t("downloads_secion_title")}</h2>
|
<div className="game-options-modal__header">
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h2>{t("downloads_secion_title")}</h2>
|
||||||
{t("downloads_section_description")}
|
<h4 className="game-options-modal__header-description">
|
||||||
</h4>
|
{t("downloads_section_description")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="game-options-modal__row">
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowRepacksModal(true)}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting || isGameDownloading || !repacks.length}
|
||||||
|
>
|
||||||
|
{t("open_download_options")}
|
||||||
|
</Button>
|
||||||
|
{game.download?.downloadPath && (
|
||||||
|
<Button
|
||||||
|
onClick={handleOpenDownloadFolder}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{t("open_download_location")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gameOptionRow}>
|
<div className="game-options-modal__danger-zone">
|
||||||
<Button
|
<div className="game-options-modal__header">
|
||||||
onClick={() => setShowRepacksModal(true)}
|
<h2>{t("danger_zone_section_title")}</h2>
|
||||||
theme="outline"
|
<h4 className="game-options-modal__danger-zone-description">
|
||||||
disabled={deleting || isGameDownloading || !repacks.length}
|
{t("danger_zone_section_description")}
|
||||||
>
|
</h4>
|
||||||
{t("open_download_options")}
|
</div>
|
||||||
</Button>
|
|
||||||
{game.download?.downloadPath && (
|
<div className="game-options-modal__danger-zone-buttons">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpenDownloadFolder}
|
onClick={() => setShowRemoveGameModal(true)}
|
||||||
theme="outline"
|
theme="danger"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
>
|
>
|
||||||
{t("open_download_location")}
|
{t("remove_from_library")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.gameOptionHeader}>
|
<Button
|
||||||
<h2>{t("danger_zone_section_title")}</h2>
|
onClick={() => setShowResetAchievementsModal(true)}
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
theme="danger"
|
||||||
{t("danger_zone_section_description")}
|
disabled={
|
||||||
</h4>
|
deleting ||
|
||||||
</div>
|
isDeletingAchievements ||
|
||||||
|
!hasAchievements ||
|
||||||
|
!userDetails
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("reset_achievements")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div className={styles.gameOptionRow}>
|
<Button
|
||||||
<Button
|
onClick={() => {
|
||||||
onClick={() => setShowRemoveGameModal(true)}
|
setShowDeleteModal(true);
|
||||||
theme="danger"
|
}}
|
||||||
disabled={deleting}
|
theme="danger"
|
||||||
>
|
disabled={
|
||||||
{t("remove_from_library")}
|
isGameDownloading || deleting || !game.download?.downloadPath
|
||||||
</Button>
|
}
|
||||||
|
>
|
||||||
<Button
|
{t("remove_files")}
|
||||||
onClick={() => setShowResetAchievementsModal(true)}
|
</Button>
|
||||||
theme="danger"
|
</div>
|
||||||
disabled={
|
|
||||||
deleting ||
|
|
||||||
isDeletingAchievements ||
|
|
||||||
!hasAchievements ||
|
|
||||||
!userDetails
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("reset_achievements")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setShowDeleteModal(true);
|
|
||||||
}}
|
|
||||||
theme="danger"
|
|
||||||
disabled={
|
|
||||||
isGameDownloading || deleting || !game.download?.downloadPath
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("remove_files")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -7,7 +7,6 @@ import type {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Link } from "@renderer/components";
|
import { Button, Link } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
|
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
|
||||||
import {
|
import {
|
||||||
@ -20,8 +19,8 @@ import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
|||||||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||||
import { buildGameAchievementPath } from "@renderer/helpers";
|
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
|
import "./sidebar.scss";
|
||||||
|
|
||||||
const achievementsPlaceholder: UserAchievement[] = [
|
const achievementsPlaceholder: UserAchievement[] = [
|
||||||
{
|
{
|
||||||
@ -64,7 +63,6 @@ export function Sidebar() {
|
|||||||
}>({ isLoading: true, data: null });
|
}>({ isLoading: true, data: null });
|
||||||
|
|
||||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||||
|
|
||||||
const [activeRequirement, setActiveRequirement] =
|
const [activeRequirement, setActiveRequirement] =
|
||||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||||
|
|
||||||
@ -72,10 +70,8 @@ export function Sidebar() {
|
|||||||
useContext(gameDetailsContext);
|
useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { showHydraCloudModal } = useSubscription();
|
const { showHydraCloudModal } = useSubscription();
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
const { formatDateTime } = useDate();
|
const { formatDateTime } = useDate();
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -118,7 +114,7 @@ export function Sidebar() {
|
|||||||
}, [objectId, shop, gameTitle]);
|
}, [objectId, shop, gameTitle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.contentSidebar}>
|
<aside className="content-sidebar">
|
||||||
{userDetails === null && (
|
{userDetails === null && (
|
||||||
<SidebarSection title={t("achievements")}>
|
<SidebarSection title={t("achievements")}>
|
||||||
<div
|
<div
|
||||||
@ -133,21 +129,21 @@ export function Sidebar() {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: "8px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LockIcon size={36} />
|
<LockIcon size={36} />
|
||||||
<h3>{t("sign_in_to_see_achievements")}</h3>
|
<h3>{t("sign_in_to_see_achievements")}</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul className={styles.list} style={{ filter: "blur(4px)" }}>
|
<ul className="list" style={{ filter: "blur(4px)" }}>
|
||||||
{achievementsPlaceholder.map((achievement, index) => (
|
{achievementsPlaceholder.map((achievement) => (
|
||||||
<li key={index}>
|
<li key={achievement.displayName}>
|
||||||
<div className={styles.listItem}>
|
<div className="list__item">
|
||||||
<img
|
<img
|
||||||
style={{ filter: "blur(8px)" }}
|
style={{ filter: "blur(8px)" }}
|
||||||
className={styles.listItemImage({
|
className={`list__item-image ${
|
||||||
unlocked: achievement.unlocked,
|
achievement.unlocked ? "" : "list__item-image--locked"
|
||||||
})}
|
}`}
|
||||||
src={achievement.icon}
|
src={achievement.icon}
|
||||||
alt={achievement.displayName}
|
alt={achievement.displayName}
|
||||||
/>
|
/>
|
||||||
@ -164,6 +160,7 @@ export function Sidebar() {
|
|||||||
</ul>
|
</ul>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userDetails && achievements && achievements.length > 0 && (
|
{userDetails && achievements && achievements.length > 0 && (
|
||||||
<SidebarSection
|
<SidebarSection
|
||||||
title={t("achievements_count", {
|
title={t("achievements_count", {
|
||||||
@ -171,10 +168,10 @@ export function Sidebar() {
|
|||||||
achievementsCount: achievements.length,
|
achievementsCount: achievements.length,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ul className={styles.list}>
|
<ul className="list">
|
||||||
{!hasActiveSubscription && (
|
{!hasActiveSubscription && (
|
||||||
<button
|
<button
|
||||||
className={styles.subscriptionRequiredButton}
|
className="subscription-required-button"
|
||||||
onClick={() => showHydraCloudModal("achievements")}
|
onClick={() => showHydraCloudModal("achievements")}
|
||||||
>
|
>
|
||||||
<CloudOfflineIcon size={16} />
|
<CloudOfflineIcon size={16} />
|
||||||
@ -182,21 +179,21 @@ export function Sidebar() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{achievements.slice(0, 4).map((achievement, index) => (
|
{achievements.slice(0, 4).map((achievement) => (
|
||||||
<li key={index}>
|
<li key={achievement.displayName}>
|
||||||
<Link
|
<Link
|
||||||
to={buildGameAchievementPath({
|
to={buildGameAchievementPath({
|
||||||
shop: shop,
|
shop: shop,
|
||||||
objectId: objectId!,
|
objectId: objectId!,
|
||||||
title: gameTitle,
|
title: gameTitle,
|
||||||
})}
|
})}
|
||||||
className={styles.listItem}
|
className="list__item"
|
||||||
title={achievement.description}
|
title={achievement.description}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className={styles.listItemImage({
|
className={`list__item-image ${
|
||||||
unlocked: achievement.unlocked,
|
achievement.unlocked ? "" : "list__item-image--locked"
|
||||||
})}
|
}`}
|
||||||
src={achievement.icon}
|
src={achievement.icon}
|
||||||
alt={achievement.displayName}
|
alt={achievement.displayName}
|
||||||
/>
|
/>
|
||||||
@ -226,17 +223,17 @@ export function Sidebar() {
|
|||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
<SidebarSection title={t("stats")}>
|
<SidebarSection title={t("stats")}>
|
||||||
<div className={styles.statsSection}>
|
<div className="stats__section">
|
||||||
<div className={styles.statsCategory}>
|
<div className="stats__category">
|
||||||
<p className={styles.statsCategoryTitle}>
|
<p className="stats__category-title">
|
||||||
<DownloadIcon size={18} />
|
<DownloadIcon size={18} />
|
||||||
{t("download_count")}
|
{t("download_count")}
|
||||||
</p>
|
</p>
|
||||||
<p>{numberFormatter.format(stats?.downloadCount)}</p>
|
<p>{numberFormatter.format(stats?.downloadCount)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.statsCategory}>
|
<div className="stats__category">
|
||||||
<p className={styles.statsCategoryTitle}>
|
<p className="stats__category-title">
|
||||||
<PeopleIcon size={18} />
|
<PeopleIcon size={18} />
|
||||||
{t("player_count")}
|
{t("player_count")}
|
||||||
</p>
|
</p>
|
||||||
@ -252,9 +249,9 @@ export function Sidebar() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SidebarSection title={t("requirements")}>
|
<SidebarSection title={t("requirements")}>
|
||||||
<div className={styles.requirementButtonContainer}>
|
<div className="requirement__button-container">
|
||||||
<Button
|
<Button
|
||||||
className={styles.requirementButton}
|
className="requirement__button"
|
||||||
onClick={() => setActiveRequirement("minimum")}
|
onClick={() => setActiveRequirement("minimum")}
|
||||||
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
||||||
>
|
>
|
||||||
@ -262,7 +259,7 @@ export function Sidebar() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={styles.requirementButton}
|
className="requirement__button"
|
||||||
onClick={() => setActiveRequirement("recommended")}
|
onClick={() => setActiveRequirement("recommended")}
|
||||||
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
||||||
>
|
>
|
||||||
@ -271,7 +268,7 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles.requirementsDetails}
|
className="requirement__details"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html:
|
__html:
|
||||||
shopDetails?.pc_requirements?.[activeRequirement] ??
|
shopDetails?.pc_requirements?.[activeRequirement] ??
|
||||||
|
Loading…
Reference in New Issue
Block a user