Merge branch 'feat/migration-to-leveldb' into feature/torbox-integration

# Conflicts:
#	src/renderer/src/pages/downloads/download-group.tsx
This commit is contained in:
Zamitto
2025-02-01 16:42:15 -03:00
40 changed files with 1624 additions and 1101 deletions

View File

@@ -29,6 +29,7 @@ import { downloadSourcesWorker } from "./workers";
import { downloadSourcesTable } from "./dexie";
import { useSubscription } from "./hooks/use-subscription";
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
import { SPACING_UNIT } from "./theme.css";
export interface AppProps {
children: React.ReactNode;
@@ -212,22 +213,22 @@ export function App() {
const id = crypto.randomUUID();
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
channel.onmessage = (event: MessageEvent<number>) => {
channel.onmessage = async (event: MessageEvent<number>) => {
const newRepacksCount = event.data;
window.electron.publishNewRepacksNotification(newRepacksCount);
updateRepacks();
downloadSourcesTable.toArray().then((downloadSources) => {
downloadSources
.filter((source) => !source.fingerprint)
.forEach((downloadSource) => {
window.electron
.putDownloadSource(downloadSource.objectIds)
.then(({ fingerprint }) => {
downloadSourcesTable.update(downloadSource.id, { fingerprint });
});
});
});
const downloadSources = await downloadSourcesTable.toArray();
downloadSources
.filter((source) => !source.fingerprint)
.forEach(async (downloadSource) => {
const { fingerprint } = await window.electron.putDownloadSource(
downloadSource.objectIds
);
downloadSourcesTable.update(downloadSource.id, { fingerprint });
});
};
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
@@ -255,7 +256,7 @@ export function App() {
return (
<>
{/* {window.electron.platform === "win32" && (
{window.electron.platform === "win32" && (
<div className={styles.titleBar}>
<h4>
Hydra
@@ -264,22 +265,25 @@ export function App() {
)}
</h4>
</div>
)} */}
<div className={styles.titleBar}>
<h4>
Hydra
{hasActiveSubscription && (
<span className={styles.cloudText}> Cloud</span>
)}
</h4>
</div>
)}
<Toast
visible={toast.visible}
message={toast.message}
type={toast.type}
onClose={handleToastClose}
/>
<div
style={{
position: "absolute",
bottom: `${26 + SPACING_UNIT * 2}px`,
right: "16px",
maxWidth: "420px",
width: "420px",
}}
>
<Toast
visible={toast.visible}
title={toast.title}
message={toast.message}
type={toast.type}
onClose={handleToastClose}
/>
</div>
<HydraCloudModal
visible={isHydraCloudModalVisible}

View File

@@ -1,152 +0,0 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const sidebar = recipe({
base: {
backgroundColor: vars.color.darkBackground,
color: vars.color.muted,
flexDirection: "column",
display: "flex",
transition: "opacity ease 0.2s",
borderRight: `solid 1px ${vars.color.border}`,
position: "relative",
overflow: "hidden",
justifyContent: "space-between",
},
variants: {
resizing: {
true: {
opacity: vars.opacity.active,
pointerEvents: "none",
},
},
darwin: {
true: {
paddingTop: `${SPACING_UNIT * 6}px`,
},
false: {
paddingTop: `${SPACING_UNIT}px`,
},
},
},
});
export const content = style({
display: "flex",
flexDirection: "column",
padding: `${SPACING_UNIT * 2}px`,
gap: `${SPACING_UNIT * 2}px`,
width: "100%",
overflow: "auto",
});
export const handle = style({
width: "5px",
height: "100%",
cursor: "col-resize",
position: "absolute",
right: "0",
});
export const menu = style({
listStyle: "none",
padding: "0",
margin: "0",
gap: `${SPACING_UNIT / 2}px`,
display: "flex",
flexDirection: "column",
overflow: "hidden",
});
export const menuItem = recipe({
base: {
transition: "all ease 0.1s",
cursor: "pointer",
textWrap: "nowrap",
display: "flex",
color: vars.color.muted,
borderRadius: "4px",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
},
variants: {
active: {
true: {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
},
muted: {
true: {
opacity: vars.opacity.disabled,
":hover": {
opacity: "1",
},
},
},
},
});
export const menuItemButton = style({
color: "inherit",
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
cursor: "pointer",
overflow: "hidden",
width: "100%",
padding: `9px ${SPACING_UNIT}px`,
});
export const menuItemButtonLabel = style({
textOverflow: "ellipsis",
overflow: "hidden",
});
export const gameIcon = style({
width: "20px",
height: "20px",
minWidth: "20px",
minHeight: "20px",
borderRadius: "4px",
backgroundSize: "cover",
});
export const sectionTitle = style({
textTransform: "uppercase",
fontWeight: "bold",
});
export const section = style({
gap: `${SPACING_UNIT * 2}px`,
display: "flex",
flexDirection: "column",
paddingBottom: `${SPACING_UNIT}px`,
});
export const helpButton = style({
color: vars.color.muted,
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
gap: "9px",
display: "flex",
alignItems: "center",
cursor: "pointer",
borderTop: `solid 1px ${vars.color.border}`,
transition: "background-color ease 0.1s",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const helpButtonIcon = style({
background: "linear-gradient(0deg, #16B195 50%, #3E62C0 100%)",
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#fff",
borderRadius: "50%",
});

View File

@@ -0,0 +1,136 @@
@use "../../scss/globals.scss";
.sidebar {
background-color: globals.$dark-background-color;
color: globals.$muted-color;
flex-direction: column;
display: flex;
transition: opacity ease 0.2s;
border-right: solid 1px globals.$border-color;
position: relative;
overflow: hidden;
padding-top: globals.$spacing-unit;
&--resizing {
opacity: globals.$active-opacity;
pointer-events: none;
}
&--darwin {
padding-top: calc(globals.$spacing-unit * 6);
}
&__content {
display: flex;
flex-direction: column;
padding: calc(globals.$spacing-unit * 2);
gap: calc(globals.$spacing-unit * 2);
width: 100%;
overflow: auto;
}
&__handle {
width: 5px;
height: 100%;
cursor: col-resize;
position: absolute;
right: 0;
}
&__menu {
list-style: none;
padding: 0;
margin: 0;
gap: calc(globals.$spacing-unit / 2);
display: flex;
flex-direction: column;
overflow: hidden;
}
&__menu-item {
transition: all ease 0.1s;
cursor: pointer;
text-wrap: nowrap;
display: flex;
color: globals.$muted-color;
border-radius: 4px;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
}
&--active {
background-color: rgba(255, 255, 255, 0.1);
}
&--muted {
opacity: globals.$disabled-opacity;
&:hover {
opacity: 1;
}
}
}
&__menu-item-button {
color: inherit;
display: flex;
align-items: center;
gap: globals.$spacing-unit;
cursor: pointer;
overflow: hidden;
width: 100%;
padding: 9px globals.$spacing-unit;
}
&__menu-item-button-label {
text-overflow: ellipsis;
overflow: hidden;
}
&__game-icon {
width: 20px;
height: 20px;
min-width: 20px;
min-height: 20px;
border-radius: 4px;
background-size: cover;
}
&__section-title {
text-transform: uppercase;
font-weight: bold;
}
&__section {
gap: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
padding-bottom: globals.$spacing-unit;
}
&__help-button {
color: globals.$muted-color;
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
gap: 9px;
display: flex;
align-items: center;
cursor: pointer;
border-top: solid 1px globals.$border-color;
transition: background-color ease 0.1s;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
}
}
&__help-button-icon {
background: linear-gradient(0deg, #16b195 50%, #3e62c0 100%);
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 50%;
}
}

View File

@@ -14,12 +14,14 @@ import {
import { routes } from "./routes";
import * as styles from "./sidebar.css";
import "./sidebar.scss";
import { buildGameDetailsPath } from "@renderer/helpers";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es";
import cn from "classnames";
import { CommentDiscussionIcon } from "@primer/octicons-react";
const SIDEBAR_MIN_WIDTH = 200;
@@ -168,9 +170,9 @@ export function Sidebar() {
return (
<aside
ref={sidebarRef}
className={styles.sidebar({
resizing: isResizing,
darwin: window.electron.platform === "darwin",
className={cn("sidebar", {
"sidebar--resizing": isResizing,
"sidebar--darwin": window.electron.platform === "darwin",
})}
style={{
width: sidebarWidth,
@@ -179,23 +181,28 @@ export function Sidebar() {
}}
>
<div
style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}
style={{
display: "flex",
flexDirection: "column",
overflow: "hidden",
flex: 1,
}}
>
<SidebarProfile />
<div className={styles.content}>
<section className={styles.section}>
<ul className={styles.menu}>
<div className="sidebar__content">
<section className="sidebar__section">
<ul className="sidebar__menu">
{routes.map(({ nameKey, path, render }) => (
<li
key={nameKey}
className={styles.menuItem({
active: location.pathname === path,
className={cn("sidebar__menu-item", {
"sidebar__menu-item--active": location.pathname === path,
})}
>
<button
type="button"
className={styles.menuItemButton}
className="sidebar__menu-item-button"
onClick={() => handleSidebarItemClick(path)}
>
{render()}
@@ -206,8 +213,8 @@ export function Sidebar() {
</ul>
</section>
<section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small>
<section className="sidebar__section">
<small className="sidebar__section-title">{t("my_library")}</small>
<TextField
ref={filterRef}
@@ -216,34 +223,35 @@ export function Sidebar() {
theme="dark"
/>
<ul className={styles.menu}>
<ul className="sidebar__menu">
{filteredLibrary.map((game) => (
<li
key={`${game.shop}-${game.objectId}`}
className={styles.menuItem({
active:
key={game.id}
className={cn("sidebar__menu-item", {
"sidebar__menu-item--active":
location.pathname ===
`/game/${game.shop}/${game.objectId}`,
muted: game.download?.status === "removed",
"sidebar__menu-item--muted":
game.download?.status === "removed",
})}
>
<button
type="button"
className={styles.menuItemButton}
className="sidebar__menu-item-button"
onClick={(event) => handleSidebarGameClick(event, game)}
>
{game.iconUrl ? (
<img
className={styles.gameIcon}
className="sidebar__game-icon"
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className={styles.gameIcon} />
<SteamLogo className="sidebar__game-icon" />
)}
<span className={styles.menuItemButtonLabel}>
<span className="sidebar__menu-item-button-label">
{getGameTitle(game)}
</span>
</button>
@@ -257,10 +265,10 @@ export function Sidebar() {
{hasActiveSubscription && (
<button
type="button"
className={styles.helpButton}
className="sidebar__help-button"
data-open-support-chat
>
<div className={styles.helpButtonIcon}>
<div className="sidebar__help-button-icon">
<CommentDiscussionIcon size={14} />
</div>
<span>{t("need_help")}</span>
@@ -269,7 +277,7 @@ export function Sidebar() {
<button
type="button"
className={styles.handle}
className="sidebar__handle"
onMouseDown={handleMouseDown}
/>
</aside>

View File

@@ -1,87 +0,0 @@
import { keyframes, style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { recipe } from "@vanilla-extract/recipes";
const TOAST_HEIGHT = 80;
export const slideIn = keyframes({
"0%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
"100%": { transform: "translateY(0)" },
});
export const slideOut = keyframes({
"0%": { transform: `translateY(0)` },
"100%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
});
export const toast = recipe({
base: {
animationDuration: "0.2s",
animationTimingFunction: "ease-in-out",
maxHeight: TOAST_HEIGHT,
position: "fixed",
backgroundColor: vars.color.background,
borderRadius: "4px",
border: `solid 1px ${vars.color.border}`,
right: `${SPACING_UNIT * 2}px`,
/* Bottom panel height + 16px */
bottom: `${26 + SPACING_UNIT * 2}px`,
overflow: "hidden",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
zIndex: vars.zIndex.toast,
maxWidth: "500px",
},
variants: {
closing: {
true: {
animationName: slideOut,
transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)`,
},
false: {
animationName: slideIn,
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: "5px",
"::-webkit-progress-bar": {
backgroundColor: vars.color.darkBackground,
},
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
},
});
export const closeButton = style({
color: vars.color.body,
cursor: "pointer",
padding: "0",
margin: "0",
});
export const successIcon = style({
color: vars.color.success,
});
export const errorIcon = style({
color: vars.color.danger,
});
export const warningIcon = style({
color: vars.color.warning,
});

View File

@@ -0,0 +1,85 @@
@use "../../scss/globals.scss";
.toast {
animation-duration: 0.2s;
animation-timing-function: ease-in-out;
position: absolute;
background-color: globals.$dark-background-color;
border-radius: 4px;
border: solid 1px globals.$border-color;
right: 0;
bottom: 0;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
z-index: globals.$toast-z-index;
max-width: 420px;
animation-name: enter;
transform: translateY(0);
&--closing {
animation-name: exit;
transform: translateY(100%);
}
&__content {
display: flex;
gap: calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
justify-content: center;
align-items: center;
}
&__progress {
width: 100%;
height: 5px;
&::-webkit-progress-bar {
background-color: globals.$dark-background-color;
}
&::-webkit-progress-value {
background-color: globals.$muted-color;
}
}
&__close-button {
color: globals.$body-color;
cursor: pointer;
padding: 0;
margin: 0;
transition: color 0.2s ease-in-out;
&:hover {
color: globals.$muted-color;
}
}
&__icon {
&--success {
color: globals.$success-color;
}
&--error {
color: globals.$danger-color;
}
&--warning {
color: globals.$warning-color;
}
}
}
@keyframes enter {
0% {
opacity: 0;
transform: translateY(100%);
}
}
@keyframes exit {
0% {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -6,19 +6,28 @@ import {
XIcon,
} from "@primer/octicons-react";
import * as styles from "./toast.css";
import { SPACING_UNIT } from "@renderer/theme.css";
import "./toast.scss";
import cn from "classnames";
export interface ToastProps {
visible: boolean;
message: string;
title: string;
message?: string;
type: "success" | "error" | "warning";
duration?: number;
onClose: () => void;
}
const INITIAL_PROGRESS = 100;
export function Toast({ visible, message, type, onClose }: ToastProps) {
export function Toast({
visible,
title,
message,
type,
duration = 2500,
onClose,
}: Readonly<ToastProps>) {
const [isClosing, setIsClosing] = useState(false);
const [progress, setProgress] = useState(INITIAL_PROGRESS);
@@ -31,7 +40,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
closingAnimation.current = requestAnimationFrame(
function animateClosing(time) {
if (time - zero <= 200) {
if (time - zero <= 150) {
closingAnimation.current = requestAnimationFrame(animateClosing);
} else {
onClose();
@@ -43,17 +52,13 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
useEffect(() => {
if (visible) {
const zero = performance.now();
progressAnimation.current = requestAnimationFrame(
function animateProgress(time) {
const elapsed = time - zero;
const progress = Math.min(elapsed / 2500, 1);
const progress = Math.min(elapsed / duration, 1);
const currentValue =
INITIAL_PROGRESS + (0 - INITIAL_PROGRESS) * progress;
setProgress(currentValue);
if (progress < 1) {
progressAnimation.current = requestAnimationFrame(animateProgress);
} else {
@@ -70,37 +75,62 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
setIsClosing(false);
};
}
return () => {};
}, [startAnimateClosing, visible]);
}, [startAnimateClosing, duration, visible]);
if (!visible) return null;
return (
<div className={styles.toast({ closing: isClosing })}>
<div className={styles.toastContent}>
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
{type === "success" && (
<CheckCircleFillIcon className={styles.successIcon} />
)}
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
{type === "warning" && <AlertIcon className={styles.warningIcon} />}
<span style={{ fontWeight: "bold" }}>{message}</span>
</div>
<button
type="button"
className={styles.closeButton}
onClick={startAnimateClosing}
aria-label="Close toast"
<div
className={cn("toast", {
"toast--closing": isClosing,
})}
>
<div className="toast__content">
<div
style={{
display: "flex",
gap: `8px`,
flexDirection: "column",
}}
>
<XIcon />
</button>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: `8px`,
}}
>
{type === "success" && (
<CheckCircleFillIcon className="toast__icon--success" />
)}
{type === "error" && (
<XCircleFillIcon className="toast__icon--error" />
)}
{type === "warning" && (
<AlertIcon className="toast__icon--warning" />
)}
<span style={{ fontWeight: "bold", flex: 1 }}>{title}</span>
<button
type="button"
className="toast__close-button"
onClick={startAnimateClosing}
aria-label="Close toast"
>
<XIcon />
</button>
</div>
{message && <p>{message}</p>}
</div>
</div>
<progress className={styles.progress} value={progress} max={100} />
<progress className="toast__progress" value={progress} max={100} />
</div>
);
}

View File

@@ -3,12 +3,14 @@ import type { PayloadAction } from "@reduxjs/toolkit";
import { ToastProps } from "@renderer/components/toast/toast";
export interface ToastState {
message: string;
title: string;
message?: string;
type: ToastProps["type"];
visible: boolean;
}
const initialState: ToastState = {
title: "",
message: "",
type: "success",
visible: false,
@@ -19,6 +21,7 @@ export const toastSlice = createSlice({
initialState,
reducers: {
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
state.title = action.payload.title;
state.message = action.payload.message;
state.type = action.payload.type;
state.visible = true;

View File

@@ -6,9 +6,10 @@ export function useToast() {
const dispatch = useAppDispatch();
const showSuccessToast = useCallback(
(message: string) => {
(title: string, message?: string) => {
dispatch(
showToast({
title,
message,
type: "success",
})
@@ -18,9 +19,10 @@ export function useToast() {
);
const showErrorToast = useCallback(
(message: string) => {
(title: string, message?: string) => {
dispatch(
showToast({
title,
message,
type: "error",
})
@@ -30,9 +32,10 @@ export function useToast() {
);
const showWarningToast = useCallback(
(message: string) => {
(title: string, message?: string) => {
dispatch(
showToast({
title,
message,
type: "warning",
})

View File

@@ -1,43 +1,38 @@
import { useDate } from "@renderer/hooks";
import type { UserAchievement } from "@types";
import { useTranslation } from "react-i18next";
import * as styles from "./achievements.css";
import "./achievements.scss";
import { EyeClosedIcon } from "@primer/octicons-react";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useSubscription } from "@renderer/hooks/use-subscription";
import { vars } from "@renderer/theme.css";
interface AchievementListProps {
achievements: UserAchievement[];
}
export function AchievementList({ achievements }: AchievementListProps) {
export function AchievementList({
achievements,
}: Readonly<AchievementListProps>) {
const { t } = useTranslation("achievement");
const { showHydraCloudModal } = useSubscription();
const { formatDateTime } = useDate();
return (
<ul className={styles.list}>
<ul className="achievements__list">
{achievements.map((achievement) => (
<li
key={achievement.name}
className={styles.listItem}
style={{ display: "flex" }}
>
<li key={achievement.name} className="achievements__item">
<img
className={styles.listItemImage({
unlocked: achievement.unlocked,
})}
className={`achievements__item-image ${!achievement.unlocked ? "achievements__item-image--locked" : ""}`}
src={achievement.icon}
alt={achievement.displayName}
loading="lazy"
/>
<div style={{ flex: 1 }}>
<h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<div className="achievements__item-content">
<h4 className="achievements__item-title">
{achievement.hidden && (
<span
style={{ display: "flex" }}
className="achievements__item-hidden-icon"
title={t("hidden_achievement_tooltip")}
>
<EyeClosedIcon size={12} />
@@ -47,48 +42,36 @@ export function AchievementList({ achievements }: AchievementListProps) {
</h4>
<p>{achievement.description}</p>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
alignItems: "flex-end",
}}
>
<div className="achievements__item-meta">
{achievement.points != undefined ? (
<div
style={{ display: "flex", alignItems: "center", gap: "4px" }}
className="achievements__item-points"
title={t("achievement_earn_points", {
points: achievement.points,
})}
>
<HydraIcon width={20} height={20} />
<p style={{ fontSize: "1.1em" }}>{achievement.points}</p>
<HydraIcon className="achievements__item-points-icon" />
<p className="achievements__item-points-value">
{achievement.points}
</p>
</div>
) : (
<button
onClick={() => showHydraCloudModal("achievements")}
style={{
display: "flex",
alignItems: "center",
gap: "4px",
cursor: "pointer",
color: vars.color.warning,
}}
title={t("achievement_earn_points", {
points: "???",
})}
className="achievements__item-points achievements__item-points--locked"
title={t("achievement_earn_points", { points: "???" })}
>
<HydraIcon width={20} height={20} />
<p style={{ fontSize: "1.1em" }}>???</p>
<HydraIcon className="achievements__item-points-icon" />
<p className="achievements__item-points-value">???</p>
</button>
)}
{achievement.unlockTime != null && (
<div
className="achievements__item-unlock-time"
title={t("unlocked_at", {
date: formatDateTime(achievement.unlockTime),
})}
style={{ whiteSpace: "nowrap", gap: "4px", display: "flex" }}
>
<small>{formatDateTime(achievement.unlockTime)}</small>
</div>

View File

@@ -0,0 +1,262 @@
@use "../../scss/globals.scss";
@use "sass:math";
$hero-height: 150px;
$logo-height: 100px;
$logo-max-width: 200px;
.achievements {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
height: 100%;
transition: all ease 0.3s;
&__hero {
width: 100%;
height: $hero-height;
min-height: $hero-height;
display: flex;
flex-direction: column;
position: relative;
transition: all ease 0.2s;
&-content {
padding: globals.$spacing-unit * 2;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
&-logo-backdrop {
width: 100%;
height: 100%;
position: absolute;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
&-image-skeleton {
height: 150px;
}
}
&__game-logo {
width: $logo-max-width;
height: $logo-height;
object-fit: contain;
transition: all ease 0.2s;
&:hover {
transform: scale(1.05);
}
}
&__container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
z-index: 1;
}
&__table-header {
width: 100%;
background-color: var(--color-dark-background);
transition: all ease 0.2s;
border-bottom: solid 1px var(--color-border);
position: sticky;
top: 0;
z-index: 1;
&--stuck {
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
}
}
&__list {
list-style: none;
margin: 0;
display: flex;
flex-direction: column;
gap: globals.$spacing-unit * 2;
padding: globals.$spacing-unit * 2;
width: 100%;
background-color: var(--color-background);
}
&__item {
display: flex;
transition: all ease 0.1s;
color: var(--color-muted);
width: 100%;
overflow: hidden;
border-radius: 4px;
padding: globals.$spacing-unit globals.$spacing-unit;
gap: globals.$spacing-unit * 2;
align-items: center;
text-align: left;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
text-decoration: none;
}
&-image {
width: 54px;
height: 54px;
border-radius: 4px;
object-fit: cover;
&--locked {
filter: grayscale(100%);
}
}
&-content {
flex: 1;
}
&-title {
display: flex;
align-items: center;
gap: 4px;
}
&-hidden-icon {
display: flex;
color: var(--color-warning);
opacity: 0.8;
&:hover {
opacity: 1;
}
svg {
width: 12px;
height: 12px;
}
}
&-eye-closed {
width: 12px;
height: 12px;
color: globals.$warning-color;
scale: 4;
}
&-meta {
display: flex;
flex-direction: column;
gap: 8px;
}
&-points {
display: flex;
align-items: center;
gap: 4px;
margin-right: 4px;
font-weight: 600;
&--locked {
cursor: pointer;
color: var(--color-warning);
}
&-icon {
width: 18px;
height: 18px;
}
&-value {
font-size: 1.1em;
}
}
&-unlock-time {
white-space: nowrap;
gap: 4px;
display: flex;
}
&-compared {
display: grid;
grid-template-columns: 3fr 1fr 1fr;
&--no-owner {
grid-template-columns: 3fr 2fr;
}
}
&-main {
display: flex;
flex-direction: row;
align-items: center;
gap: globals.$spacing-unit;
}
&-status {
display: flex;
padding: globals.$spacing-unit;
justify-content: center;
&--unlocked {
white-space: nowrap;
flex-direction: row;
gap: globals.$spacing-unit;
padding: 0;
}
}
}
&__progress-bar {
width: 100%;
height: 8px;
transition: all ease 0.2s;
&::-webkit-progress-bar {
background-color: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
&::-webkit-progress-value {
background-color: var(--color-muted);
border-radius: 4px;
}
}
&__profile-avatar {
height: 54px;
width: 54px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-background);
position: relative;
object-fit: cover;
&--small {
height: 32px;
width: 32px;
}
}
&__subscription-button {
text-decoration: none;
display: flex;
justify-content: center;
width: 100%;
gap: math.div(globals.$spacing-unit, 2);
color: var(--color-body);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}

View File

@@ -1,109 +0,0 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const downloadTitleWrapper = style({
display: "flex",
alignItems: "center",
marginBottom: `${SPACING_UNIT}px`,
gap: `${SPACING_UNIT}px`,
});
export const downloadTitle = style({
fontWeight: "bold",
cursor: "pointer",
color: vars.color.body,
textAlign: "left",
fontSize: "16px",
display: "block",
":hover": {
textDecoration: "underline",
},
});
export const downloads = style({
width: "100%",
gap: `${SPACING_UNIT * 2}px`,
display: "flex",
flexDirection: "column",
margin: "0",
padding: "0",
marginTop: `${SPACING_UNIT}px`,
});
export const downloadCover = style({
width: "280px",
minWidth: "280px",
height: "auto",
borderRight: `solid 1px ${vars.color.border}`,
position: "relative",
zIndex: "1",
});
export const downloadCoverContent = style({
width: "100%",
height: "100%",
padding: `${SPACING_UNIT}px`,
display: "flex",
alignItems: "flex-end",
justifyContent: "flex-end",
});
export const downloadCoverBackdrop = style({
width: "100%",
height: "100%",
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
display: "flex",
overflow: "hidden",
zIndex: "1",
});
export const downloadCoverImage = style({
width: "100%",
height: "100%",
position: "absolute",
zIndex: "-1",
});
export const download = style({
width: "100%",
backgroundColor: vars.color.background,
display: "flex",
borderRadius: "8px",
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
boxShadow: "0px 0px 5px 0px #000000",
transition: "all ease 0.2s",
height: "140px",
minHeight: "140px",
maxHeight: "140px",
});
export const downloadDetails = style({
display: "flex",
flexDirection: "column",
flex: "1",
justifyContent: "center",
gap: `${SPACING_UNIT / 2}px`,
fontSize: "14px",
});
export const downloadRightContent = style({
display: "flex",
padding: `${SPACING_UNIT * 2}px`,
flex: "1",
gap: `${SPACING_UNIT}px`,
background: "linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%)",
});
export const downloadActions = style({
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
});
export const downloadGroup = style({
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
});

View File

@@ -0,0 +1,140 @@
@use "../../scss/globals.scss";
.download-group {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: calc(globals.$spacing-unit * 2);
&-divider {
flex: 1;
background-color: globals.$border-color;
height: 1px;
}
&-count {
font-weight: 400;
}
}
&__title-wrapper {
display: flex;
align-items: center;
margin-bottom: globals.$spacing-unit;
gap: globals.$spacing-unit;
}
&__title {
font-weight: bold;
cursor: pointer;
color: globals.$body-color;
text-align: left;
font-size: 16px;
display: block;
&:hover {
text-decoration: underline;
}
}
&__downloads {
width: 100%;
gap: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
margin-top: globals.$spacing-unit;
}
&__item {
width: 100%;
background-color: globals.$background-color;
display: flex;
border-radius: 8px;
border: solid 1px globals.$border-color;
overflow: hidden;
box-shadow: 0px 0px 5px 0px #000000;
transition: all ease 0.2s;
height: 140px;
min-height: 140px;
max-height: 140px;
position: relative;
}
&__cover {
width: 280px;
min-width: 280px;
height: auto;
border-right: solid 1px globals.$border-color;
position: relative;
z-index: 1;
&-content {
width: 100%;
height: 100%;
padding: globals.$spacing-unit;
display: flex;
align-items: flex-end;
justify-content: flex-end;
}
&-backdrop {
width: 100%;
height: 100%;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.8) 5%,
transparent 100%
);
display: flex;
overflow: hidden;
z-index: 1;
}
&-image {
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
}
}
&__right-content {
display: flex;
padding: calc(globals.$spacing-unit * 2);
flex: 1;
gap: globals.$spacing-unit;
background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
}
&__details {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
gap: calc(globals.$spacing-unit / 2);
font-size: 14px;
}
&__actions {
display: flex;
align-items: center;
gap: globals.$spacing-unit;
}
&__menu-button {
position: absolute;
top: 12px;
right: 12px;
border-radius: 50%;
border: none;
padding: 8px;
min-height: unset;
}
}

View File

@@ -12,9 +12,8 @@ import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
import { DOWNLOADER_NAME } from "@renderer/constants";
import { useAppSelector, useDownload } from "@renderer/hooks";
import * as styles from "./download-group.css";
import "./download-group.scss";
import { useTranslation } from "react-i18next";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { useMemo } from "react";
import {
DropdownMenu,
@@ -262,44 +261,26 @@ export function DownloadGroup({
if (!library.length) return null;
return (
<div className={styles.downloadGroup}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<div className="download-group">
<div className="download-group__header">
<h2>{title}</h2>
<div
style={{
flex: 1,
backgroundColor: vars.color.border,
height: "1px",
}}
/>
<h3 style={{ fontWeight: "400" }}>{library.length}</h3>
<div className="download-group__header-divider" />
<h3 className="download-group__header-count">{library.length}</h3>
</div>
<ul className={styles.downloads}>
<ul className="download-group__downloads">
{library.map((game) => {
return (
<li
key={game.id}
className={styles.download}
style={{ position: "relative" }}
>
<div className={styles.downloadCover}>
<div className={styles.downloadCoverBackdrop}>
<li key={game.id} className="download-group__item">
<div className="download-group__cover">
<div className="download-group__cover-backdrop">
<img
src={steamUrlBuilder.library(game.objectId)}
className={styles.downloadCoverImage}
className="download-group__cover-image"
alt={game.title}
/>
<div className={styles.downloadCoverContent}>
<div className="download-group__cover-content">
{game.download?.downloader === Downloader.TorBox ? (
<div
style={{
@@ -327,12 +308,12 @@ export function DownloadGroup({
</div>
</div>
</div>
<div className={styles.downloadRightContent}>
<div className={styles.downloadDetails}>
<div className={styles.downloadTitleWrapper}>
<div className="download-group__right-content">
<div className="download-group__details">
<div className="download-group__title-wrapper">
<button
type="button"
className={styles.downloadTitle}
className="download-group__title"
onClick={() =>
navigate(
buildGameDetailsPath({
@@ -356,15 +337,7 @@ export function DownloadGroup({
sideOffset={-75}
>
<Button
style={{
position: "absolute",
top: "12px",
right: "12px",
borderRadius: "50%",
border: "none",
padding: "8px",
minHeight: "unset",
}}
className="download-group__menu-button"
theme="outline"
>
<ThreeBarsIcon />

View File

@@ -0,0 +1,15 @@
@use "../../../scss/globals.scss";
.hero-panel-playtime {
&__download-details {
gap: globals.$spacing-unit;
display: flex;
color: globals.$body-color;
align-items: center;
}
&__downloads-link {
color: globals.$body-color;
text-decoration: underline;
}
}

View File

@@ -1,24 +1,18 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import * as styles from "./hero-panel.css";
import { formatDownloadProgress } from "@renderer/helpers";
import { useDate, useDownload, useFormat } from "@renderer/hooks";
import { Link } from "@renderer/components";
import { gameDetailsContext } from "@renderer/context";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import "./hero-panel-playtime.scss";
export function HeroPanelPlaytime() {
const [lastTimePlayed, setLastTimePlayed] = useState("");
const { game, isGameRunning } = useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
const { numberFormatter } = useFormat();
const { progress, lastPacket } = useDownload();
const { formatDistance } = useDate();
useEffect(() => {
@@ -56,8 +50,8 @@ export function HeroPanelPlaytime() {
game.download?.status === "active" && lastPacket?.gameId === game.id;
const downloadInProgressInfo = (
<div className={styles.downloadDetailsRow}>
<Link to="/downloads" className={styles.downloadsLink}>
<div className="hero-panel-playtime__download-details">
<Link to="/downloads" className="hero-panel-playtime__downloads-link">
{game.download?.status === "active"
? t("download_in_progress")
: t("download_paused")}
@@ -84,7 +78,6 @@ export function HeroPanelPlaytime() {
return (
<>
<p>{t("playing_now")}</p>
{hasDownload && downloadInProgressInfo}
</>
);

View File

@@ -1,77 +0,0 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../../theme.css";
export const panel = recipe({
base: {
width: "100%",
height: "72px",
minHeight: "72px",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
backgroundColor: vars.color.darkBackground,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
transition: "all ease 0.2s",
borderBottom: `solid 1px ${vars.color.border}`,
position: "sticky",
overflow: "hidden",
top: "0",
zIndex: "2",
},
variants: {
stuck: {
true: {
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.8)",
},
},
},
});
export const content = style({
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
});
export const actions = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});
export const downloadDetailsRow = style({
gap: `${SPACING_UNIT}px`,
display: "flex",
color: vars.color.body,
alignItems: "center",
});
export const downloadsLink = style({
color: vars.color.body,
textDecoration: "underline",
});
export const progressBar = recipe({
base: {
position: "absolute",
bottom: "0",
left: "0",
width: "100%",
height: "3px",
transition: "all ease 0.2s",
"::-webkit-progress-bar": {
backgroundColor: "transparent",
},
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
},
},
variants: {
disabled: {
true: {
opacity: vars.opacity.disabled,
},
},
},
});

View File

@@ -0,0 +1,66 @@
@use "../../../scss/globals.scss";
.hero-panel {
width: 100%;
height: 72px;
min-height: 72px;
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
background-color: globals.$dark-background-color;
display: flex;
align-items: center;
justify-content: space-between;
transition: all ease 0.2s;
border-bottom: solid 1px globals.$border-color;
position: sticky;
overflow: hidden;
top: 0;
z-index: 2;
&--stuck {
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
}
&__content {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
}
&__actions {
display: flex;
gap: globals.$spacing-unit;
}
&__download-details {
gap: globals.$spacing-unit;
display: flex;
color: globals.$body-color;
align-items: center;
}
&__downloads-link {
color: globals.$body-color;
text-decoration: underline;
}
&__progress-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
transition: all ease 0.2s;
&::-webkit-progress-bar {
background-color: transparent;
}
&::-webkit-progress-value {
background-color: globals.$muted-color;
}
&--disabled {
opacity: globals.$disabled-opacity;
}
}
}

View File

@@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next";
import { useDate, useDownload } from "@renderer/hooks";
import { HeroPanelActions } from "./hero-panel-actions";
import * as styles from "./hero-panel.css";
import { HeroPanelPlaytime } from "./hero-panel-playtime";
import { gameDetailsContext } from "@renderer/context";
import "./hero-panel.scss";
export interface HeroPanelProps {
isHeaderStuck: boolean;
@@ -54,30 +54,28 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
game?.download?.status === "paused";
return (
<>
<div
style={{ backgroundColor: gameColor }}
className={styles.panel({ stuck: isHeaderStuck })}
>
<div className={styles.content}>{getInfo()}</div>
<div className={styles.actions}>
<HeroPanelActions />
</div>
{showProgressBar && (
<progress
max={1}
value={
isGameDownloading
? lastPacket?.progress
: game?.download?.progress
}
className={styles.progressBar({
disabled: game?.download?.status === "paused",
})}
/>
)}
<div
style={{ backgroundColor: gameColor }}
className={`hero-panel ${isHeaderStuck ? "hero-panel--stuck" : ""}`}
>
<div className="hero-panel__content">{getInfo()}</div>
<div className="hero-panel__actions">
<HeroPanelActions />
</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>
);
}

View File

@@ -123,8 +123,8 @@ export function DownloadSettingsModal({
.then(() => {
onClose();
})
.catch(() => {
showErrorToast(t("download_error"));
.catch((error) => {
showErrorToast(t("download_error"), error.message);
})
.finally(() => {
setDownloadStarting(false);

View File

@@ -21,7 +21,7 @@ export function GameOptionsModal({
visible,
game,
onClose,
}: GameOptionsModalProps) {
}: Readonly<GameOptionsModalProps>) {
const { t } = useTranslation("game_details");
const { showSuccessToast, showErrorToast } = useToast();

View File

@@ -0,0 +1,174 @@
@use "../../../scss/globals.scss";
.content-sidebar {
border-left: solid 1px globals.$border-color;
background-color: globals.$dark-background-color;
width: 100%;
height: 100%;
@media (min-width: 1024px) {
max-width: 300px;
width: 100%;
}
@media (min-width: 1280px) {
width: 100%;
max-width: 400px;
}
}
.requirement {
&__button-container {
width: 100%;
display: flex;
}
&__button {
border: solid 1px globals.$border-color;
border-left: none;
border-right: none;
border-radius: 0;
width: 100%;
}
&__details {
padding: calc(globals.$spacing-unit * 2);
line-height: 22px;
font-size: globals.$body-font-size;
a {
display: flex;
color: globals.$body-color;
}
}
&__details-skeleton {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
padding: calc(globals.$spacing-unit * 2);
font-size: globals.$body-font-size;
}
}
.how-long-to-beat {
&__categories-list {
margin: 0;
padding: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
}
&__category {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
background: linear-gradient(
90deg,
transparent 20%,
rgb(255 255 255 / 2%) 100%
);
border-radius: 4px;
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
border: solid 1px globals.$border-color;
}
&__category-label {
color: globals.$muted-color;
}
&__category-skeleton {
border: solid 1px globals.$border-color;
border-radius: 4px;
height: 76px;
}
}
.stats {
&__section {
display: flex;
gap: calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 2);
justify-content: space-between;
transition: max-height ease 0.5s;
overflow: hidden;
@media (min-width: 1024px) {
flex-direction: column;
}
@media (min-width: 1280px) {
flex-direction: row;
}
}
&__category-title {
font-size: globals.$small-font-size;
font-weight: bold;
display: flex;
align-items: center;
gap: globals.$spacing-unit;
}
&__category {
display: flex;
flex-direction: row;
gap: calc(globals.$spacing-unit / 2);
justify-content: space-between;
align-items: center;
}
}
.list {
list-style: none;
margin: 0;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 2);
&__item {
display: flex;
cursor: pointer;
transition: all ease 0.1s;
color: globals.$muted-color;
width: 100%;
overflow: hidden;
border-radius: 4px;
padding: globals.$spacing-unit;
gap: calc(globals.$spacing-unit * 2);
align-items: center;
text-align: left;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
text-decoration: none;
}
}
&__item-image {
width: 54px;
height: 54px;
border-radius: 4px;
object-fit: cover;
&--locked {
filter: grayscale(100%);
}
}
}
.subscription-required-button {
text-decoration: none;
display: flex;
justify-content: center;
width: 100%;
gap: calc(globals.$spacing-unit / 2);
color: globals.$warning-color;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}

View File

@@ -7,7 +7,6 @@ import type {
import { useTranslation } from "react-i18next";
import { Button, Link } from "@renderer/components";
import * as styles from "./sidebar.css";
import { gameDetailsContext } from "@renderer/context";
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
import {
@@ -20,8 +19,8 @@ import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { howLongToBeatEntriesTable } from "@renderer/dexie";
import { SidebarSection } from "../sidebar-section/sidebar-section";
import { buildGameAchievementPath } from "@renderer/helpers";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useSubscription } from "@renderer/hooks/use-subscription";
import "./sidebar.scss";
const achievementsPlaceholder: UserAchievement[] = [
{
@@ -64,7 +63,6 @@ export function Sidebar() {
}>({ isLoading: true, data: null });
const { userDetails, hasActiveSubscription } = useUserDetails();
const [activeRequirement, setActiveRequirement] =
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
@@ -72,10 +70,8 @@ export function Sidebar() {
useContext(gameDetailsContext);
const { showHydraCloudModal } = useSubscription();
const { t } = useTranslation("game_details");
const { formatDateTime } = useDate();
const { numberFormatter } = useFormat();
useEffect(() => {
@@ -118,7 +114,7 @@ export function Sidebar() {
}, [objectId, shop, gameTitle]);
return (
<aside className={styles.contentSidebar}>
<aside className="content-sidebar">
{userDetails === null && (
<SidebarSection title={t("achievements")}>
<div
@@ -133,21 +129,21 @@ export function Sidebar() {
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
gap: "8px",
}}
>
<LockIcon size={36} />
<h3>{t("sign_in_to_see_achievements")}</h3>
</div>
<ul className={styles.list} style={{ filter: "blur(4px)" }}>
{achievementsPlaceholder.map((achievement, index) => (
<li key={index}>
<div className={styles.listItem}>
<ul className="list" style={{ filter: "blur(4px)" }}>
{achievementsPlaceholder.map((achievement) => (
<li key={achievement.displayName}>
<div className="list__item">
<img
style={{ filter: "blur(8px)" }}
className={styles.listItemImage({
unlocked: achievement.unlocked,
})}
className={`list__item-image ${
achievement.unlocked ? "" : "list__item-image--locked"
}`}
src={achievement.icon}
alt={achievement.displayName}
/>
@@ -164,6 +160,7 @@ export function Sidebar() {
</ul>
</SidebarSection>
)}
{userDetails && achievements && achievements.length > 0 && (
<SidebarSection
title={t("achievements_count", {
@@ -171,10 +168,10 @@ export function Sidebar() {
achievementsCount: achievements.length,
})}
>
<ul className={styles.list}>
<ul className="list">
{!hasActiveSubscription && (
<button
className={styles.subscriptionRequiredButton}
className="subscription-required-button"
onClick={() => showHydraCloudModal("achievements")}
>
<CloudOfflineIcon size={16} />
@@ -182,21 +179,21 @@ export function Sidebar() {
</button>
)}
{achievements.slice(0, 4).map((achievement, index) => (
<li key={index}>
{achievements.slice(0, 4).map((achievement) => (
<li key={achievement.displayName}>
<Link
to={buildGameAchievementPath({
shop: shop,
objectId: objectId!,
title: gameTitle,
})}
className={styles.listItem}
className="list__item"
title={achievement.description}
>
<img
className={styles.listItemImage({
unlocked: achievement.unlocked,
})}
className={`list__item-image ${
achievement.unlocked ? "" : "list__item-image--locked"
}`}
src={achievement.icon}
alt={achievement.displayName}
/>
@@ -226,17 +223,17 @@ export function Sidebar() {
{stats && (
<SidebarSection title={t("stats")}>
<div className={styles.statsSection}>
<div className={styles.statsCategory}>
<p className={styles.statsCategoryTitle}>
<div className="stats__section">
<div className="stats__category">
<p className="stats__category-title">
<DownloadIcon size={18} />
{t("download_count")}
</p>
<p>{numberFormatter.format(stats?.downloadCount)}</p>
</div>
<div className={styles.statsCategory}>
<p className={styles.statsCategoryTitle}>
<div className="stats__category">
<p className="stats__category-title">
<PeopleIcon size={18} />
{t("player_count")}
</p>
@@ -252,9 +249,9 @@ export function Sidebar() {
/>
<SidebarSection title={t("requirements")}>
<div className={styles.requirementButtonContainer}>
<div className="requirement__button-container">
<Button
className={styles.requirementButton}
className="requirement__button"
onClick={() => setActiveRequirement("minimum")}
theme={activeRequirement === "minimum" ? "primary" : "outline"}
>
@@ -262,7 +259,7 @@ export function Sidebar() {
</Button>
<Button
className={styles.requirementButton}
className="requirement__button"
onClick={() => setActiveRequirement("recommended")}
theme={activeRequirement === "recommended" ? "primary" : "outline"}
>
@@ -271,7 +268,7 @@ export function Sidebar() {
</div>
<div
className={styles.requirementsDetails}
className="requirement__details"
dangerouslySetInnerHTML={{
__html:
shopDetails?.pc_requirements?.[activeRequirement] ??

View File

@@ -28,13 +28,15 @@ export function SettingsBehavior() {
useEffect(() => {
if (userPreferences) {
setForm({
preferQuitInsteadOfHiding: userPreferences.preferQuitInsteadOfHiding,
runAtStartup: userPreferences.runAtStartup,
startMinimized: userPreferences.startMinimized,
disableNsfwAlert: userPreferences.disableNsfwAlert,
seedAfterDownloadComplete: userPreferences.seedAfterDownloadComplete,
preferQuitInsteadOfHiding:
userPreferences.preferQuitInsteadOfHiding ?? false,
runAtStartup: userPreferences.runAtStartup ?? false,
startMinimized: userPreferences.startMinimized ?? false,
disableNsfwAlert: userPreferences.disableNsfwAlert ?? false,
seedAfterDownloadComplete:
userPreferences.seedAfterDownloadComplete ?? false,
showHiddenAchievementsDescription:
userPreferences.showHiddenAchievementsDescription,
userPreferences.showHiddenAchievementsDescription ?? false,
});
}
}, [userPreferences]);

View File

@@ -65,18 +65,20 @@ export function SettingsGeneral() {
(language) => language === userPreferences.language
) ??
languageKeys.find((language) => {
return language.startsWith(userPreferences.language.split("-")[0]);
return language.startsWith(
userPreferences.language?.split("-")[0] ?? "en"
);
});
setForm((prev) => ({
...prev,
downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath,
downloadNotificationsEnabled:
userPreferences.downloadNotificationsEnabled,
userPreferences.downloadNotificationsEnabled ?? false,
repackUpdatesNotificationsEnabled:
userPreferences.repackUpdatesNotificationsEnabled,
userPreferences.repackUpdatesNotificationsEnabled ?? false,
achievementNotificationsEnabled:
userPreferences.achievementNotificationsEnabled,
userPreferences.achievementNotificationsEnabled ?? false,
language: language ?? "en",
}));
}

View File

@@ -16,6 +16,11 @@ $spacing-unit: 8px;
$toast-z-index: 5;
$bottom-panel-z-index: 3;
$title-bar-z-index: 1900000001;
$title-bar-z-index: 4;
$backdrop-z-index: 4;
$modal-z-index: 5;
$body-font-size: 14px;
$small-font-size: 12px;
$app-container: app-container;