mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
121 lines
3.6 KiB
TypeScript
121 lines
3.6 KiB
TypeScript
import { LibraryGame } from "@types";
|
|
import { useGameCard } from "@renderer/hooks";
|
|
import { memo, useState, useEffect } from "react";
|
|
import {
|
|
ClockIcon,
|
|
AlertFillIcon,
|
|
TrophyIcon,
|
|
ImageIcon,
|
|
} from "@primer/octicons-react";
|
|
import "./library-game-card.scss";
|
|
|
|
interface LibraryGameCardProps {
|
|
game: LibraryGame;
|
|
onMouseEnter: () => void;
|
|
onMouseLeave: () => void;
|
|
onContextMenu: (
|
|
game: LibraryGame,
|
|
position: { x: number; y: number }
|
|
) => void;
|
|
onShowTooltip?: (gameId: string) => void;
|
|
onHideTooltip?: () => void;
|
|
}
|
|
|
|
export const LibraryGameCard = memo(function LibraryGameCard({
|
|
game,
|
|
onMouseEnter,
|
|
onMouseLeave,
|
|
onContextMenu,
|
|
}: Readonly<LibraryGameCardProps>) {
|
|
const { formatPlayTime, handleCardClick, handleContextMenuClick } =
|
|
useGameCard(game, onContextMenu);
|
|
|
|
const coverImage = game.coverImageUrl?.replaceAll("\\", "/") ?? "";
|
|
|
|
const [imageError, setImageError] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setImageError(false);
|
|
}, [coverImage]);
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
className="library-game-card__wrapper"
|
|
title={game.title}
|
|
onClick={handleCardClick}
|
|
onContextMenu={handleContextMenuClick}
|
|
>
|
|
<div className="library-game-card__overlay">
|
|
<div className="library-game-card__top-section">
|
|
<div className="library-game-card__playtime">
|
|
{game.hasManuallyUpdatedPlaytime ? (
|
|
<AlertFillIcon
|
|
size={11}
|
|
className="library-game-card__manual-playtime"
|
|
/>
|
|
) : (
|
|
<ClockIcon size={11} />
|
|
)}
|
|
<span className="library-game-card__playtime-long">
|
|
{formatPlayTime(game.playTimeInMilliseconds)}
|
|
</span>
|
|
<span className="library-game-card__playtime-short">
|
|
{formatPlayTime(game.playTimeInMilliseconds, true)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{(game.achievementCount ?? 0) > 0 && (
|
|
<div className="library-game-card__achievements">
|
|
<div className="library-game-card__achievement-header">
|
|
<div className="library-game-card__achievements-gap">
|
|
<TrophyIcon
|
|
size={13}
|
|
className="library-game-card__achievement-trophy"
|
|
/>
|
|
<span className="library-game-card__achievement-count">
|
|
{game.unlockedAchievementCount ?? 0} /{" "}
|
|
{game.achievementCount ?? 0}
|
|
</span>
|
|
</div>
|
|
<span className="library-game-card__achievement-percentage">
|
|
{Math.round(
|
|
((game.unlockedAchievementCount ?? 0) /
|
|
(game.achievementCount ?? 1)) *
|
|
100
|
|
)}
|
|
%
|
|
</span>
|
|
</div>
|
|
<div className="library-game-card__achievement-progress">
|
|
<div
|
|
className="library-game-card__achievement-bar"
|
|
style={{
|
|
width: `${((game.unlockedAchievementCount ?? 0) / (game.achievementCount ?? 1)) * 100}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{imageError || !coverImage ? (
|
|
<div className="library-game-card__cover-placeholder">
|
|
<ImageIcon size={48} />
|
|
</div>
|
|
) : (
|
|
<img
|
|
src={coverImage}
|
|
alt={game.title}
|
|
className="library-game-card__game-image"
|
|
loading="lazy"
|
|
onError={() => setImageError(true)}
|
|
/>
|
|
)}
|
|
</button>
|
|
);
|
|
});
|