mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 09:13:57 +00:00
277 lines
7.3 KiB
TypeScript
277 lines
7.3 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 "./sidebar.scss";
|
|
|
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
|
|
|
import { SidebarProfile } from "./sidebar-profile";
|
|
import { sortBy } from "lodash-es";
|
|
import cn from "classnames";
|
|
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
|
import { SidebarGameItem } from "./sidebar-game-item";
|
|
|
|
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?.queued) return t("queued", { title: game.title });
|
|
|
|
if (game.download?.status === "paused")
|
|
return t("paused", { 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"));
|
|
}
|
|
}
|
|
};
|
|
|
|
const favoriteGames = useMemo(() => {
|
|
return sortedLibrary.filter((game) => game.favorite);
|
|
}, [sortedLibrary]);
|
|
|
|
return (
|
|
<aside
|
|
ref={sidebarRef}
|
|
className={cn("sidebar", {
|
|
"sidebar--resizing": isResizing,
|
|
"sidebar--darwin": window.electron.platform === "darwin",
|
|
})}
|
|
style={{
|
|
width: sidebarWidth,
|
|
minWidth: sidebarWidth,
|
|
maxWidth: sidebarWidth,
|
|
}}
|
|
>
|
|
<div className="sidebar__container">
|
|
<SidebarProfile />
|
|
|
|
<div className="sidebar__content">
|
|
<section className="sidebar__section">
|
|
<ul className="sidebar__menu">
|
|
{routes.map(({ nameKey, path, render }) => (
|
|
<li
|
|
key={nameKey}
|
|
className={cn("sidebar__menu-item", {
|
|
"sidebar__menu-item--active": location.pathname === path,
|
|
})}
|
|
>
|
|
<button
|
|
type="button"
|
|
className="sidebar__menu-item-button"
|
|
onClick={() => handleSidebarItemClick(path)}
|
|
>
|
|
{render()}
|
|
<span>{t(nameKey)}</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
{favoriteGames.length > 0 && (
|
|
<section className="sidebar__section">
|
|
<small className="sidebar__section-title">{t("favorites")}</small>
|
|
|
|
<ul className="sidebar__menu">
|
|
{favoriteGames.map((game) => (
|
|
<SidebarGameItem
|
|
key={game.id}
|
|
game={game}
|
|
handleSidebarGameClick={handleSidebarGameClick}
|
|
getGameTitle={getGameTitle}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
|
|
<section className="sidebar__section">
|
|
<small className="sidebar__section-title">{t("my_library")}</small>
|
|
|
|
<TextField
|
|
ref={filterRef}
|
|
placeholder={t("filter")}
|
|
onChange={handleFilter}
|
|
theme="dark"
|
|
/>
|
|
|
|
<ul className="sidebar__menu">
|
|
{filteredLibrary
|
|
.filter((game) => !game.favorite)
|
|
.map((game) => (
|
|
<SidebarGameItem
|
|
key={game.id}
|
|
game={game}
|
|
handleSidebarGameClick={handleSidebarGameClick}
|
|
getGameTitle={getGameTitle}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
{hasActiveSubscription && (
|
|
<button
|
|
type="button"
|
|
className="sidebar__help-button"
|
|
data-open-support-chat
|
|
>
|
|
<div className="sidebar__help-button-icon">
|
|
<CommentDiscussionIcon size={14} />
|
|
</div>
|
|
<span>{t("need_help")}</span>
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
className="sidebar__handle"
|
|
onMouseDown={handleMouseDown}
|
|
/>
|
|
</aside>
|
|
);
|
|
}
|