mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 05:46:17 +00:00
Compare commits
3 Commits
9769eecec6
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e578047929 | ||
|
|
e49d885b30 | ||
|
|
cb01301a0d |
@@ -19,12 +19,12 @@
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"typecheck": "yarn run typecheck:node && yarn run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"build": "yarn run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps && node ./scripts/postinstall.cjs",
|
||||
"build:unpack": "npm run build && electron-builder --dir",
|
||||
"build:unpack": "yarn run build && electron-builder --dir",
|
||||
"build:win": "electron-vite build && electron-builder --win",
|
||||
"build:mac": "electron-vite build && electron-builder --mac",
|
||||
"build:linux": "electron-vite build && electron-builder --linux",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"library": "Library",
|
||||
"downloads": "Downloads",
|
||||
"settings": "Settings",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Available",
|
||||
"my_library": "My library",
|
||||
"downloading_metadata": "{{title}} (Downloading metadata…)",
|
||||
"paused": "{{title}} (Paused)",
|
||||
@@ -414,7 +415,11 @@
|
||||
"resume_seeding": "Resume seeding",
|
||||
"options": "Manage",
|
||||
"extract": "Extract files",
|
||||
"extracting": "Extracting files…"
|
||||
"extracting": "Extracting files…",
|
||||
"network": "Network",
|
||||
"peak": "Peak",
|
||||
"seeds": "Seeds",
|
||||
"peers": "Peers"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloads path",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"library": "Librería",
|
||||
"downloads": "Descargas",
|
||||
"settings": "Ajustes",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Disponible",
|
||||
"my_library": "Mi Librería",
|
||||
"downloading_metadata": "{{title}} (Descargando metadatos…)",
|
||||
"paused": "{{title}} (Pausado)",
|
||||
@@ -414,7 +415,11 @@
|
||||
"resume_seeding": "Continuar sembrando",
|
||||
"options": "Administrar",
|
||||
"extract": "Extraer archivos",
|
||||
"extracting": "Extrayendo archivos…"
|
||||
"extracting": "Extrayendo archivos…",
|
||||
"network": "Red",
|
||||
"peak": "Pico",
|
||||
"seeds": "Seeds",
|
||||
"peers": "Peers"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Ruta de descarga",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"library": "Biblioteca",
|
||||
"downloads": "Downloads",
|
||||
"settings": "Ajustes",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Já disponível",
|
||||
"my_library": "Biblioteca",
|
||||
"downloading_metadata": "{{title}} (Baixando metadados…)",
|
||||
"paused": "{{title}} (Pausado)",
|
||||
@@ -402,7 +403,11 @@
|
||||
"resume_seeding": "Semear",
|
||||
"options": "Gerenciar",
|
||||
"extract": "Extrair arquivos",
|
||||
"extracting": "Extraindo arquivos…"
|
||||
"extracting": "Extraindo arquivos…",
|
||||
"network": "Rede",
|
||||
"peak": "Pico",
|
||||
"seeds": "Seeds",
|
||||
"peers": "Peers"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Diretório dos downloads",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Transferências",
|
||||
"settings": "Definições",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Já disponível",
|
||||
"my_library": "Biblioteca",
|
||||
"downloading_metadata": "{{title}} (A transferir metadados…)",
|
||||
"paused": "{{title}} (Em pausa)",
|
||||
@@ -229,7 +230,13 @@
|
||||
"seeding": "A semear",
|
||||
"stop_seeding": "Parar de semear",
|
||||
"resume_seeding": "Semear",
|
||||
"options": "Opções"
|
||||
"options": "Opções",
|
||||
"extract": "Extrair ficheiros",
|
||||
"extracting": "A extrair ficheiros…",
|
||||
"network": "Rede",
|
||||
"peak": "Pico",
|
||||
"seeds": "Seeds",
|
||||
"peers": "Peers"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Local das transferências",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"library": "Библиотека",
|
||||
"downloads": "Загрузки",
|
||||
"settings": "Настройки",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Доступно",
|
||||
"my_library": "Библиотека",
|
||||
"downloading_metadata": "{{title}} (Загрузка метаданных…)",
|
||||
"paused": "{{title}} (Приостановлено)",
|
||||
@@ -414,7 +415,11 @@
|
||||
"resume_seeding": "Продолжить раздачу",
|
||||
"options": "Управлять",
|
||||
"extract": "Распаковать файлы",
|
||||
"extracting": "Распаковка файлов…"
|
||||
"extracting": "Распаковка файлов…",
|
||||
"network": "Сеть",
|
||||
"peak": "Пик",
|
||||
"seeds": "Seeds",
|
||||
"peers": "Peers"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Путь загрузок",
|
||||
|
||||
@@ -354,6 +354,8 @@ export class WindowManager {
|
||||
public static async createNotificationWindow() {
|
||||
if (this.notificationWindow) return;
|
||||
|
||||
if (process.platform === "darwin") return;
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | undefined>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
|
||||
@@ -32,4 +32,15 @@ export const routes = [
|
||||
nameKey: "settings",
|
||||
render: () => <GearIcon />,
|
||||
},
|
||||
{
|
||||
path: "https://hydrawrapped.com",
|
||||
nameKey: "hydra_2025_wrapped",
|
||||
render: () => (
|
||||
<img
|
||||
src="https://cdn.losbroxas.org/thumbnail_hydra_badge2_fb01af31e3.png"
|
||||
alt="Hydra 2025 Wrapped"
|
||||
style={{ width: 16, height: 16 }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -88,6 +88,34 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&--wrapped {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(74, 144, 226, 0.25) 0%,
|
||||
rgba(123, 104, 238, 0.2) 25%,
|
||||
rgba(59, 130, 246, 0.25) 50%,
|
||||
rgba(96, 165, 250, 0.2) 75%,
|
||||
rgba(74, 144, 226, 0.25) 100%
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
animation: wrapped-gradient-flow 8s ease infinite;
|
||||
color: globals.$muted-color;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(74, 144, 226, 0.35) 0%,
|
||||
rgba(123, 104, 238, 0.3) 25%,
|
||||
rgba(59, 130, 246, 0.35) 50%,
|
||||
rgba(96, 165, 250, 0.3) 75%,
|
||||
rgba(74, 144, 226, 0.35) 100%
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__menu-item-button {
|
||||
@@ -106,6 +134,21 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__menu-item-marquee {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__menu-item-marquee-content {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&__menu-item-marquee-content span {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__game-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -228,3 +271,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wrapped-gradient-flow {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-50% - 1em));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
import { sortBy } from "lodash-es";
|
||||
import cn from "classnames";
|
||||
import { logger } from "@renderer/logger";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
CommentDiscussionIcon,
|
||||
PlayIcon,
|
||||
@@ -238,8 +240,32 @@ export function Sidebar() {
|
||||
return game.title;
|
||||
};
|
||||
|
||||
const handleSidebarItemClick = (path: string) => {
|
||||
if (path !== location.pathname) {
|
||||
const handleSidebarItemClick = async (path: string) => {
|
||||
if (path.startsWith("http")) {
|
||||
if (path === "https://hydrawrapped.com") {
|
||||
try {
|
||||
const auth = await window.electron.getAuth();
|
||||
if (auth) {
|
||||
const payload = {
|
||||
accessToken: auth.accessToken,
|
||||
refreshToken: auth.refreshToken,
|
||||
expiresIn: 3600,
|
||||
};
|
||||
const base64Payload = btoa(JSON.stringify(payload));
|
||||
window.electron.openExternal(
|
||||
`${path}?payload=${encodeURIComponent(base64Payload)}`
|
||||
);
|
||||
} else {
|
||||
window.electron.openExternal(path);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to get auth for wrapped:", error);
|
||||
window.electron.openExternal(path);
|
||||
}
|
||||
} else {
|
||||
window.electron.openExternal(path);
|
||||
}
|
||||
} else if (path !== location.pathname) {
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
@@ -297,7 +323,10 @@ export function Sidebar() {
|
||||
<li
|
||||
key={nameKey}
|
||||
className={cn("sidebar__menu-item", {
|
||||
"sidebar__menu-item--active": location.pathname === path,
|
||||
"sidebar__menu-item--active":
|
||||
!path.startsWith("http") && location.pathname === path,
|
||||
"sidebar__menu-item--wrapped":
|
||||
nameKey === "hydra_2025_wrapped",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
@@ -306,7 +335,33 @@ export function Sidebar() {
|
||||
onClick={() => handleSidebarItemClick(path)}
|
||||
>
|
||||
{render()}
|
||||
<span>{t(nameKey)}</span>
|
||||
{nameKey === "hydra_2025_wrapped" ? (
|
||||
<div className="sidebar__menu-item-marquee">
|
||||
<motion.div
|
||||
className="sidebar__menu-item-marquee-content"
|
||||
animate={{
|
||||
x: ["0%", "-50%"],
|
||||
}}
|
||||
transition={{
|
||||
x: {
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
duration: 8,
|
||||
ease: "linear",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{t(nameKey)}
|
||||
</span>
|
||||
<span>
|
||||
{t(nameKey)}
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<span>{t(nameKey)}</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -305,9 +305,11 @@ function HeroDownloadView({
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span className="download-group__progress-percentage">
|
||||
<AnimatedPercentage value={currentProgress} />
|
||||
</span>
|
||||
{(!lastPacket?.isCheckingFiles || currentProgress > 0) && (
|
||||
<span className="download-group__progress-percentage">
|
||||
<AnimatedPercentage value={currentProgress} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="download-group__progress-bar">
|
||||
<div
|
||||
@@ -358,7 +360,7 @@ function HeroDownloadView({
|
||||
</span>
|
||||
<div className="download-group__stat-content">
|
||||
<span className="download-group__stat-label">
|
||||
{t("network")}:
|
||||
{t("network")}
|
||||
</span>
|
||||
<span className="download-group__stat-value">
|
||||
{isGameDownloading ? formatSpeed(downloadSpeed) : "0 B/s"}
|
||||
@@ -371,37 +373,38 @@ function HeroDownloadView({
|
||||
<GraphIcon size={16} />
|
||||
</span>
|
||||
<div className="download-group__stat-content">
|
||||
<span className="download-group__stat-label">{t("peak")}:</span>
|
||||
<span className="download-group__stat-label">{t("peak")}</span>
|
||||
<span className="download-group__stat-value">
|
||||
{peakSpeed > 0 ? formatSpeed(peakSpeed) : "0 B/s"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{game.download?.downloader === Downloader.Torrent &&
|
||||
isGameDownloading &&
|
||||
lastPacket &&
|
||||
(lastPacket.numSeeds > 0 || lastPacket.numPeers > 0) && (
|
||||
<div className="download-group__stat-item">
|
||||
<div className="download-group__stat-content">
|
||||
<span className="download-group__stat-label">
|
||||
Seeds:{" "}
|
||||
<span className="download-group__stat-value">
|
||||
{lastPacket.numSeeds}
|
||||
</span>
|
||||
, Peers:{" "}
|
||||
<span className="download-group__stat-value">
|
||||
{lastPacket.numPeers}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{game.download?.downloader && (
|
||||
<div className="download-group__stat-item">
|
||||
<div className="download-group__stat-content">
|
||||
<div
|
||||
className="download-group__stat-content"
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Badge>{DOWNLOADER_NAME[game.download.downloader]}</Badge>
|
||||
{game.download?.downloader === Downloader.Torrent &&
|
||||
isGameDownloading &&
|
||||
lastPacket &&
|
||||
(lastPacket.numSeeds > 0 || lastPacket.numPeers > 0) && (
|
||||
<span className="download-group__stat-label">
|
||||
{t("seeds")}{" "}
|
||||
<span className="download-group__stat-value">
|
||||
{lastPacket.numSeeds}
|
||||
</span>
|
||||
, {t("peers")}{" "}
|
||||
<span className="download-group__stat-value">
|
||||
{lastPacket.numPeers}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -436,6 +439,7 @@ export function DownloadGroup({
|
||||
seedingStatus,
|
||||
}: Readonly<DownloadGroupProps>) {
|
||||
const { t } = useTranslation("downloads");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
@@ -867,12 +871,36 @@ export function DownloadGroup({
|
||||
{downloadInfo.map(({ game, size, progress, isSeeding: seeding }) => {
|
||||
return (
|
||||
<li key={game.id} className="download-group__simple-card">
|
||||
<div className="download-group__simple-thumbnail">
|
||||
<button
|
||||
type="button"
|
||||
className="download-group__simple-thumbnail"
|
||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<img src={game.libraryImageUrl || ""} alt={game.title} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="download-group__simple-info">
|
||||
<h3 className="download-group__simple-title">{game.title}</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="download-group__simple-title"
|
||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
cursor: "pointer",
|
||||
textAlign: "left",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{game.title}
|
||||
</button>
|
||||
<div className="download-group__simple-meta">
|
||||
<div className="download-group__simple-meta-row">
|
||||
<Badge>{DOWNLOADER_NAME[game.download!.downloader]}</Badge>
|
||||
|
||||
@@ -87,12 +87,16 @@ export function LibraryTab({
|
||||
|
||||
<ul className="profile-content__games-grid">
|
||||
{pinnedGames?.map((game) => (
|
||||
<li key={game.objectId} style={{ listStyle: "none" }}>
|
||||
<li
|
||||
key={game.objectId}
|
||||
style={{ listStyle: "none" }}
|
||||
className="user-library-game__wrapper"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
</li>
|
||||
@@ -134,6 +138,9 @@ export function LibraryTab({
|
||||
<motion.li
|
||||
key={`${sortBy}-${game.objectId}`}
|
||||
style={{ listStyle: "none" }}
|
||||
className="user-library-game__wrapper"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
initial={
|
||||
isNewGame
|
||||
? { opacity: 0.5, y: 15, scale: 0.96 }
|
||||
@@ -160,8 +167,6 @@ export function LibraryTab({
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
</motion.li>
|
||||
|
||||
@@ -25,16 +25,12 @@ import "./user-library-game-card.scss";
|
||||
interface UserLibraryGameCardProps {
|
||||
game: UserGame;
|
||||
statIndex: number;
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
sortBy?: string;
|
||||
}
|
||||
|
||||
export function UserLibraryGameCard({
|
||||
game,
|
||||
statIndex,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
sortBy,
|
||||
}: UserLibraryGameCardProps) {
|
||||
const { userProfile, isMe, getUserLibraryGames } =
|
||||
@@ -130,129 +126,119 @@ export function UserLibraryGameCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<li
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className="user-library-game__wrapper"
|
||||
<button
|
||||
type="button"
|
||||
className="user-library-game__cover"
|
||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||
title={isTooltipHovered ? undefined : game.title}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="user-library-game__cover"
|
||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||
>
|
||||
<div className="user-library-game__overlay">
|
||||
{isMe && (
|
||||
<div className="user-library-game__actions-container">
|
||||
<button
|
||||
type="button"
|
||||
className="user-library-game__pin-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleGamePinned();
|
||||
}}
|
||||
disabled={isPinning}
|
||||
>
|
||||
{game.isPinned ? (
|
||||
<PinSlashIcon size={12} />
|
||||
) : (
|
||||
<PinIcon size={12} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="user-library-game__playtime"
|
||||
data-tooltip-place="top"
|
||||
data-tooltip-content={
|
||||
game.hasManuallyUpdatedPlaytime
|
||||
? t("manual_playtime_tooltip")
|
||||
: undefined
|
||||
}
|
||||
data-tooltip-id={game.objectId}
|
||||
>
|
||||
{game.hasManuallyUpdatedPlaytime ? (
|
||||
<AlertFillIcon
|
||||
size={11}
|
||||
className="user-library-game__manual-playtime"
|
||||
/>
|
||||
) : (
|
||||
<ClockIcon size={11} />
|
||||
)}
|
||||
<span className="user-library-game__playtime-long">
|
||||
{formatPlayTime(game.playTimeInSeconds)}
|
||||
</span>
|
||||
<span className="user-library-game__playtime-short">
|
||||
{formatPlayTime(game.playTimeInSeconds, true)}
|
||||
</span>
|
||||
<div className="user-library-game__overlay">
|
||||
{isMe && (
|
||||
<div className="user-library-game__actions-container">
|
||||
<button
|
||||
type="button"
|
||||
className="user-library-game__pin-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleGamePinned();
|
||||
}}
|
||||
disabled={isPinning}
|
||||
>
|
||||
{game.isPinned ? (
|
||||
<PinSlashIcon size={12} />
|
||||
) : (
|
||||
<PinIcon size={12} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="user-library-game__playtime"
|
||||
data-tooltip-place="top"
|
||||
data-tooltip-content={
|
||||
game.hasManuallyUpdatedPlaytime
|
||||
? t("manual_playtime_tooltip")
|
||||
: undefined
|
||||
}
|
||||
data-tooltip-id={game.objectId}
|
||||
>
|
||||
{game.hasManuallyUpdatedPlaytime ? (
|
||||
<AlertFillIcon
|
||||
size={11}
|
||||
className="user-library-game__manual-playtime"
|
||||
/>
|
||||
) : (
|
||||
<ClockIcon size={11} />
|
||||
)}
|
||||
<span className="user-library-game__playtime-long">
|
||||
{formatPlayTime(game.playTimeInSeconds)}
|
||||
</span>
|
||||
<span className="user-library-game__playtime-short">
|
||||
{formatPlayTime(game.playTimeInSeconds, true)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{userProfile?.hasActiveSubscription &&
|
||||
game.achievementCount > 0 && (
|
||||
<div className="user-library-game__stats">
|
||||
<div className="user-library-game__stats-header">
|
||||
<div className="user-library-game__stats-content">
|
||||
<div
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{game.unlockedAchievementCount} /{" "}
|
||||
{game.achievementCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{game.achievementsPointsEarnedSum > 0 && (
|
||||
<div
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={16} height={16} />
|
||||
{formatAchievementPoints(
|
||||
game.achievementsPointsEarnedSum
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
||||
<div className="user-library-game__stats">
|
||||
<div className="user-library-game__stats-header">
|
||||
<div className="user-library-game__stats-content">
|
||||
<div
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
game.unlockedAchievementCount / game.achievementCount,
|
||||
1
|
||||
)}
|
||||
{game.unlockedAchievementCount} / {game.achievementCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<progress
|
||||
max={1}
|
||||
value={
|
||||
game.unlockedAchievementCount / game.achievementCount
|
||||
}
|
||||
className="user-library-game__achievements-progress"
|
||||
/>
|
||||
{game.achievementsPointsEarnedSum > 0 && (
|
||||
<div
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={16} height={16} />
|
||||
{formatAchievementPoints(
|
||||
game.achievementsPointsEarnedSum
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{imageError || !game.coverImageUrl ? (
|
||||
<div className="user-library-game__cover-placeholder">
|
||||
<ImageIcon size={48} />
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
game.unlockedAchievementCount / game.achievementCount,
|
||||
1
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<progress
|
||||
max={1}
|
||||
value={game.unlockedAchievementCount / game.achievementCount}
|
||||
className="user-library-game__achievements-progress"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={game.coverImageUrl}
|
||||
alt={game.title}
|
||||
className="user-library-game__game-image"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
{imageError || !game.coverImageUrl ? (
|
||||
<div className="user-library-game__cover-placeholder">
|
||||
<ImageIcon size={48} />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={game.coverImageUrl}
|
||||
alt={game.title}
|
||||
className="user-library-game__game-image"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<Tooltip
|
||||
id={game.objectId}
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user