mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
279 lines
7.4 KiB
TypeScript
279 lines
7.4 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
|
|
import type { LibraryGame } from "@types";
|
|
|
|
import { TextField } from "@renderer/components";
|
|
import {
|
|
useDownload,
|
|
useLibrary,
|
|
useToast,
|
|
useUserDetails,
|
|
} from "@renderer/hooks";
|
|
|
|
import { routes } from "./routes";
|
|
|
|
import * as styles from "./sidebar.css";
|
|
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 { CommentDiscussionIcon } from "@primer/octicons-react";
|
|
|
|
const SIDEBAR_MIN_WIDTH = 200;
|
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
|
const SIDEBAR_MAX_WIDTH = 450;
|
|
|
|
const initialSidebarWidth = window.localStorage.getItem("sidebarWidth");
|
|
|
|
export function Sidebar() {
|
|
const filterRef = useRef<HTMLInputElement>(null);
|
|
|
|
const { t } = useTranslation("sidebar");
|
|
const { library, updateLibrary } = useLibrary();
|
|
const navigate = useNavigate();
|
|
|
|
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
|
|
|
const [isResizing, setIsResizing] = useState(false);
|
|
const [sidebarWidth, setSidebarWidth] = useState(
|
|
initialSidebarWidth ? Number(initialSidebarWidth) : SIDEBAR_INITIAL_WIDTH
|
|
);
|
|
|
|
const location = useLocation();
|
|
|
|
const sortedLibrary = useMemo(() => {
|
|
return sortBy(library, (game) => game.title);
|
|
}, [library]);
|
|
|
|
const { hasActiveSubscription } = useUserDetails();
|
|
|
|
const { lastPacket, progress } = useDownload();
|
|
|
|
const { showWarningToast } = useToast();
|
|
|
|
useEffect(() => {
|
|
updateLibrary();
|
|
}, [lastPacket?.gameId, updateLibrary]);
|
|
|
|
const sidebarRef = useRef<HTMLElement>(null);
|
|
|
|
const cursorPos = useRef({ x: 0 });
|
|
const sidebarInitialWidth = useRef(0);
|
|
|
|
const handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (
|
|
event
|
|
) => {
|
|
setIsResizing(true);
|
|
cursorPos.current.x = event.screenX;
|
|
sidebarInitialWidth.current =
|
|
sidebarRef.current?.clientWidth || SIDEBAR_INITIAL_WIDTH;
|
|
};
|
|
|
|
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
|
setFilteredLibrary(
|
|
sortedLibrary.filter((game) =>
|
|
game.title
|
|
.toLowerCase()
|
|
.includes(event.target.value.toLocaleLowerCase())
|
|
)
|
|
);
|
|
};
|
|
|
|
useEffect(() => {
|
|
setFilteredLibrary(sortedLibrary);
|
|
|
|
if (filterRef.current) {
|
|
filterRef.current.value = "";
|
|
}
|
|
}, [sortedLibrary]);
|
|
|
|
useEffect(() => {
|
|
window.onmousemove = (event: MouseEvent) => {
|
|
if (isResizing) {
|
|
const cursorXDelta = event.screenX - cursorPos.current.x;
|
|
const newWidth = Math.max(
|
|
SIDEBAR_MIN_WIDTH,
|
|
Math.min(
|
|
sidebarInitialWidth.current + cursorXDelta,
|
|
SIDEBAR_MAX_WIDTH
|
|
)
|
|
);
|
|
|
|
setSidebarWidth(newWidth);
|
|
window.localStorage.setItem("sidebarWidth", String(newWidth));
|
|
}
|
|
};
|
|
|
|
window.onmouseup = () => {
|
|
if (isResizing) setIsResizing(false);
|
|
};
|
|
|
|
return () => {
|
|
window.onmouseup = null;
|
|
window.onmousemove = null;
|
|
};
|
|
}, [isResizing]);
|
|
|
|
const getGameTitle = (game: LibraryGame) => {
|
|
if (lastPacket?.gameId === game.id) {
|
|
return t("downloading", {
|
|
title: game.title,
|
|
percentage: progress,
|
|
});
|
|
}
|
|
|
|
if (game.download?.status === "paused")
|
|
return t("paused", { title: game.title });
|
|
|
|
if (game.download) {
|
|
return t("queued", { title: game.title });
|
|
}
|
|
|
|
return game.title;
|
|
};
|
|
|
|
const handleSidebarItemClick = (path: string) => {
|
|
if (path !== location.pathname) {
|
|
navigate(path);
|
|
}
|
|
};
|
|
|
|
const handleSidebarGameClick = (
|
|
event: React.MouseEvent,
|
|
game: LibraryGame
|
|
) => {
|
|
const path = buildGameDetailsPath({
|
|
...game,
|
|
objectId: game.objectId,
|
|
});
|
|
if (path !== location.pathname) {
|
|
navigate(path);
|
|
}
|
|
|
|
if (event.detail === 2) {
|
|
if (game.executablePath) {
|
|
window.electron.openGame(
|
|
game.shop,
|
|
game.objectId,
|
|
game.executablePath,
|
|
game.launchOptions
|
|
);
|
|
} else {
|
|
showWarningToast(t("game_has_no_executable"));
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<aside
|
|
ref={sidebarRef}
|
|
className={styles.sidebar({
|
|
resizing: isResizing,
|
|
darwin: window.electron.platform === "darwin",
|
|
})}
|
|
style={{
|
|
width: sidebarWidth,
|
|
minWidth: sidebarWidth,
|
|
maxWidth: sidebarWidth,
|
|
}}
|
|
>
|
|
<div
|
|
style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}
|
|
>
|
|
<SidebarProfile />
|
|
|
|
<div className={styles.content}>
|
|
<section className={styles.section}>
|
|
<ul className={styles.menu}>
|
|
{routes.map(({ nameKey, path, render }) => (
|
|
<li
|
|
key={nameKey}
|
|
className={styles.menuItem({
|
|
active: location.pathname === path,
|
|
})}
|
|
>
|
|
<button
|
|
type="button"
|
|
className={styles.menuItemButton}
|
|
onClick={() => handleSidebarItemClick(path)}
|
|
>
|
|
{render()}
|
|
<span>{t(nameKey)}</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
<section className={styles.section}>
|
|
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
|
|
|
<TextField
|
|
ref={filterRef}
|
|
placeholder={t("filter")}
|
|
onChange={handleFilter}
|
|
theme="dark"
|
|
/>
|
|
|
|
<ul className={styles.menu}>
|
|
{filteredLibrary.map((game) => (
|
|
<li
|
|
key={`${game.shop}-${game.objectId}`}
|
|
className={styles.menuItem({
|
|
active:
|
|
location.pathname ===
|
|
`/game/${game.shop}/${game.objectId}`,
|
|
muted: game.download?.status === "removed",
|
|
})}
|
|
>
|
|
<button
|
|
type="button"
|
|
className={styles.menuItemButton}
|
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
|
>
|
|
{game.iconUrl ? (
|
|
<img
|
|
className={styles.gameIcon}
|
|
src={game.iconUrl}
|
|
alt={game.title}
|
|
loading="lazy"
|
|
/>
|
|
) : (
|
|
<SteamLogo className={styles.gameIcon} />
|
|
)}
|
|
|
|
<span className={styles.menuItemButtonLabel}>
|
|
{getGameTitle(game)}
|
|
</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
{hasActiveSubscription && (
|
|
<button
|
|
type="button"
|
|
className={styles.helpButton}
|
|
data-open-support-chat
|
|
>
|
|
<div className={styles.helpButtonIcon}>
|
|
<CommentDiscussionIcon size={14} />
|
|
</div>
|
|
<span>{t("need_help")}</span>
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
className={styles.handle}
|
|
onMouseDown={handleMouseDown}
|
|
/>
|
|
</aside>
|
|
);
|
|
}
|