Compare commits

..

3 Commits

Author SHA1 Message Date
Chubby Granny Chaser
e578047929 feat: add translation key for Hydra Wrapped 2025 in multiple languages and implement sidebar route with marquee effect
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-12-08 15:22:05 +00:00
Chubby Granny Chaser
e49d885b30 chore: update package.json to use yarn commands for type checking and building
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-11-30 15:21:50 +00:00
Chubby Granny Chaser
cb01301a0d feat: add new translation keys for network statistics in multiple languages 2025-11-30 15:07:32 +00:00
21 changed files with 357 additions and 239 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "hydralauncher", "name": "hydralauncher",
"version": "3.7.6", "version": "3.7.5",
"description": "Hydra", "description": "Hydra",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Los Broxas", "author": "Los Broxas",
@@ -19,12 +19,12 @@
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.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", "start": "electron-vite preview",
"dev": "electron-vite dev", "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", "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:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac", "build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux", "build:linux": "electron-vite build && electron-builder --linux",

View File

@@ -16,6 +16,7 @@
"library": "Library", "library": "Library",
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Settings", "settings": "Settings",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Available",
"my_library": "My library", "my_library": "My library",
"downloading_metadata": "{{title}} (Downloading metadata…)", "downloading_metadata": "{{title}} (Downloading metadata…)",
"paused": "{{title}} (Paused)", "paused": "{{title}} (Paused)",
@@ -414,7 +415,11 @@
"resume_seeding": "Resume seeding", "resume_seeding": "Resume seeding",
"options": "Manage", "options": "Manage",
"extract": "Extract files", "extract": "Extract files",
"extracting": "Extracting files…" "extracting": "Extracting files…",
"network": "Network",
"peak": "Peak",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Downloads path", "downloads_path": "Downloads path",

View File

@@ -16,6 +16,7 @@
"library": "Librería", "library": "Librería",
"downloads": "Descargas", "downloads": "Descargas",
"settings": "Ajustes", "settings": "Ajustes",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Disponible",
"my_library": "Mi Librería", "my_library": "Mi Librería",
"downloading_metadata": "{{title}} (Descargando metadatos…)", "downloading_metadata": "{{title}} (Descargando metadatos…)",
"paused": "{{title}} (Pausado)", "paused": "{{title}} (Pausado)",
@@ -414,7 +415,11 @@
"resume_seeding": "Continuar sembrando", "resume_seeding": "Continuar sembrando",
"options": "Administrar", "options": "Administrar",
"extract": "Extraer archivos", "extract": "Extraer archivos",
"extracting": "Extrayendo archivos…" "extracting": "Extrayendo archivos…",
"network": "Red",
"peak": "Pico",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Ruta de descarga", "downloads_path": "Ruta de descarga",
@@ -458,7 +463,6 @@
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas", "description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
"button_delete_all_sources": "Eliminar todo", "button_delete_all_sources": "Eliminar todo",
"added_download_source": "Añadir fuente de descarga", "added_download_source": "Añadir fuente de descarga",
"adding": "Añadiendo…",
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas", "download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
"insert_valid_json_url": "Introducí una URL de json válida", "insert_valid_json_url": "Introducí una URL de json válida",
"found_download_option_zero": "Sin opciones de descargas encontrada", "found_download_option_zero": "Sin opciones de descargas encontrada",
@@ -564,19 +568,6 @@
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.", "debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego", "enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego", "autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
"change_achievement_sound": "Cambiar sonido de logro",
"download_source_already_exists": "Esta fuente de descarga URL ya existe.",
"download_source_failed": "Error",
"download_source_matched": "Actualizado",
"download_source_matching": "Actualizando",
"download_source_no_information": "Sin información disponible",
"download_source_pending_matching": "Actualizando pronto",
"download_sources_synced_successfully": "Todas las fuentes de descarga están sincronizadas",
"failed_add_download_source": "Error al añadir la fuente de descarga. Por favor intentá de nuevo.",
"hydra_cloud": "Hydra Cloud",
"preview_sound": "Vista previa de sonido",
"remove_achievement_sound": "Eliminar sonido de logros",
"removed_all_download_sources": "Todas las fuentes de descarga eliminadas",
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego" "hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
}, },
"notifications": { "notifications": {

View File

@@ -16,6 +16,7 @@
"library": "Biblioteca", "library": "Biblioteca",
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Ajustes", "settings": "Ajustes",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Já disponível",
"my_library": "Biblioteca", "my_library": "Biblioteca",
"downloading_metadata": "{{title}} (Baixando metadados…)", "downloading_metadata": "{{title}} (Baixando metadados…)",
"paused": "{{title}} (Pausado)", "paused": "{{title}} (Pausado)",
@@ -402,7 +403,11 @@
"resume_seeding": "Semear", "resume_seeding": "Semear",
"options": "Gerenciar", "options": "Gerenciar",
"extract": "Extrair arquivos", "extract": "Extrair arquivos",
"extracting": "Extraindo arquivos…" "extracting": "Extraindo arquivos…",
"network": "Rede",
"peak": "Pico",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Diretório dos downloads", "downloads_path": "Diretório dos downloads",

View File

@@ -15,6 +15,7 @@
"catalogue": "Catálogo", "catalogue": "Catálogo",
"downloads": "Transferências", "downloads": "Transferências",
"settings": "Definições", "settings": "Definições",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Já disponível",
"my_library": "Biblioteca", "my_library": "Biblioteca",
"downloading_metadata": "{{title}} (A transferir metadados…)", "downloading_metadata": "{{title}} (A transferir metadados…)",
"paused": "{{title}} (Em pausa)", "paused": "{{title}} (Em pausa)",
@@ -229,7 +230,13 @@
"seeding": "A semear", "seeding": "A semear",
"stop_seeding": "Parar de semear", "stop_seeding": "Parar de semear",
"resume_seeding": "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": { "settings": {
"downloads_path": "Local das transferências", "downloads_path": "Local das transferências",

View File

@@ -16,6 +16,7 @@
"library": "Библиотека", "library": "Библиотека",
"downloads": "Загрузки", "downloads": "Загрузки",
"settings": "Настройки", "settings": "Настройки",
"hydra_2025_wrapped": "Hydra Wrapped 2025 Доступно",
"my_library": "Библиотека", "my_library": "Библиотека",
"downloading_metadata": "{{title}} (Загрузка метаданных…)", "downloading_metadata": "{{title}} (Загрузка метаданных…)",
"paused": "{{title}} (Приостановлено)", "paused": "{{title}} (Приостановлено)",
@@ -414,7 +415,11 @@
"resume_seeding": "Продолжить раздачу", "resume_seeding": "Продолжить раздачу",
"options": "Управлять", "options": "Управлять",
"extract": "Распаковать файлы", "extract": "Распаковать файлы",
"extracting": "Распаковка файлов…" "extracting": "Распаковка файлов…",
"network": "Сеть",
"peak": "Пик",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Путь загрузок", "downloads_path": "Путь загрузок",

View File

@@ -33,7 +33,9 @@ export const loadState = async () => {
await import("./events"); await import("./events");
Aria2.spawn(); if (process.platform !== "darwin") {
Aria2.spawn();
}
if (userPreferences?.realDebridApiToken) { if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences.realDebridApiToken); RealDebridClient.authorize(userPreferences.realDebridApiToken);

View File

@@ -7,12 +7,9 @@ export class Aria2 {
private static process: cp.ChildProcess | null = null; private static process: cp.ChildProcess | null = null;
public static spawn() { public static spawn() {
const binaryPath = const binaryPath = app.isPackaged
process.platform === "darwin" ? path.join(process.resourcesPath, "aria2c")
? "aria2c" : path.join(__dirname, "..", "..", "binaries", "aria2c");
: app.isPackaged
? path.join(process.resourcesPath, "aria2c")
: path.join(__dirname, "..", "..", "binaries", "aria2c");
this.process = cp.spawn( this.process = cp.spawn(
binaryPath, binaryPath,

View File

@@ -36,13 +36,16 @@ export class GofileApi {
} }
public static async getDownloadLink(id: string) { public static async getDownloadLink(id: string) {
const searchParams = new URLSearchParams({
wt: WT,
});
const response = await axios.get<{ const response = await axios.get<{
status: string; status: string;
data: GofileContentsResponse; data: GofileContentsResponse;
}>(`https://api.gofile.io/contents/${id}`, { }>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
headers: { headers: {
Authorization: `Bearer ${this.token}`, Authorization: `Bearer ${this.token}`,
"X-Website-Token": WT,
}, },
}); });

View File

@@ -58,13 +58,7 @@ export class HydraApi {
const decodedBase64 = atob(payload as string); const decodedBase64 = atob(payload as string);
const jsonData = JSON.parse(decodedBase64); const jsonData = JSON.parse(decodedBase64);
const { const { accessToken, expiresIn, refreshToken } = jsonData;
accessToken,
expiresIn,
refreshToken,
featurebaseJwt,
workwondersJwt,
} = jsonData;
const now = new Date(); const now = new Date();
@@ -91,8 +85,6 @@ export class HydraApi {
accessToken, accessToken,
refreshToken, refreshToken,
tokenExpirationTimestamp, tokenExpirationTimestamp,
featurebaseJwt,
workwondersJwt,
}, },
{ valueEncoding: "json" } { valueEncoding: "json" }
); );

View File

@@ -138,8 +138,7 @@ export class WindowManager {
(details, callback) => { (details, callback) => {
if ( if (
details.webContentsId !== this.mainWindow?.webContents.id || details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot") || details.url.includes("chatwoot")
details.url.includes("workwonders")
) { ) {
return callback(details); return callback(details);
} }
@@ -160,8 +159,7 @@ export class WindowManager {
if ( if (
details.webContentsId !== this.mainWindow?.webContents.id || details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("featurebase") || details.url.includes("featurebase") ||
details.url.includes("chatwoot") || details.url.includes("chatwoot")
details.url.includes("workwonders")
) { ) {
return callback(details); return callback(details);
} }
@@ -356,6 +354,8 @@ export class WindowManager {
public static async createNotificationWindow() { public static async createNotificationWindow() {
if (this.notificationWindow) return; if (this.notificationWindow) return;
if (process.platform === "darwin") return;
const userPreferences = await db.get<string, UserPreferences | undefined>( const userPreferences = await db.get<string, UserPreferences | undefined>(
levelKeys.userPreferences, levelKeys.userPreferences,
{ {

View File

@@ -122,10 +122,10 @@ export function BottomPanel() {
</button> </button>
<button <button
data-open-workwonders-changelog-mini data-featurebase-changelog
className="bottom-panel__version-button" className="bottom-panel__version-button"
> >
<small> <small data-featurebase-changelog>
{sessionHash ? `${sessionHash} -` : ""} v{version} &quot; {sessionHash ? `${sessionHash} -` : ""} v{version} &quot;
{VERSION_CODENAME}&quot; {VERSION_CODENAME}&quot;
</small> </small>

View File

@@ -32,4 +32,15 @@ export const routes = [
nameKey: "settings", nameKey: "settings",
render: () => <GearIcon />, 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 }}
/>
),
},
]; ];

View File

@@ -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 { &__menu-item-button {
@@ -106,6 +134,21 @@
overflow: hidden; 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 { &__game-icon {
width: 20px; width: 20px;
height: 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));
}
}

View File

@@ -22,6 +22,8 @@ import { buildGameDetailsPath } from "@renderer/helpers";
import { SidebarProfile } from "./sidebar-profile"; import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es"; import { sortBy } from "lodash-es";
import cn from "classnames"; import cn from "classnames";
import { logger } from "@renderer/logger";
import { motion } from "framer-motion";
import { import {
CommentDiscussionIcon, CommentDiscussionIcon,
PlayIcon, PlayIcon,
@@ -238,8 +240,32 @@ export function Sidebar() {
return game.title; return game.title;
}; };
const handleSidebarItemClick = (path: string) => { const handleSidebarItemClick = async (path: string) => {
if (path !== location.pathname) { 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); navigate(path);
} }
}; };
@@ -297,7 +323,10 @@ export function Sidebar() {
<li <li
key={nameKey} key={nameKey}
className={cn("sidebar__menu-item", { 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 <button
@@ -306,7 +335,33 @@ export function Sidebar() {
onClick={() => handleSidebarItemClick(path)} onClick={() => handleSidebarItemClick(path)}
> >
{render()} {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)} &nbsp;&nbsp;&nbsp;&nbsp;
</span>
<span>
{t(nameKey)} &nbsp;&nbsp;&nbsp;&nbsp;
</span>
</motion.div>
</div>
) : (
<span>{t(nameKey)}</span>
)}
</button> </button>
</li> </li>
))} ))}

View File

@@ -108,11 +108,16 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
transition: scale 0.2s ease; transition: opacity 0.2s ease;
outline: none; outline: none;
&:hover { &:hover {
scale: 1.05; opacity: 0.8;
}
&:focus,
&:focus-visible {
outline: none;
} }
} }
@@ -390,21 +395,6 @@
flex-shrink: 0; flex-shrink: 0;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
border: 1px solid globals.$border-color; border: 1px solid globals.$border-color;
padding: 0;
cursor: pointer;
transition:
opacity 0.2s ease,
transform 0.2s ease;
&:hover {
opacity: 0.9;
}
&:focus,
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 2px;
}
img { img {
width: 100%; width: 100%;
@@ -421,21 +411,6 @@
gap: calc(globals.$spacing-unit / 1); gap: calc(globals.$spacing-unit / 1);
} }
&__simple-title-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
text-align: left;
width: 100%;
transition: opacity 0.2s ease;
&:focus,
&:focus-visible {
outline: none;
}
}
&__simple-title { &__simple-title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;

View File

@@ -305,9 +305,11 @@ function HeroDownloadView({
)} )}
</span> </span>
)} )}
<span className="download-group__progress-percentage"> {(!lastPacket?.isCheckingFiles || currentProgress > 0) && (
<AnimatedPercentage value={currentProgress} /> <span className="download-group__progress-percentage">
</span> <AnimatedPercentage value={currentProgress} />
</span>
)}
</div> </div>
<div className="download-group__progress-bar"> <div className="download-group__progress-bar">
<div <div
@@ -358,7 +360,7 @@ function HeroDownloadView({
</span> </span>
<div className="download-group__stat-content"> <div className="download-group__stat-content">
<span className="download-group__stat-label"> <span className="download-group__stat-label">
{t("network")}: {t("network")}
</span> </span>
<span className="download-group__stat-value"> <span className="download-group__stat-value">
{isGameDownloading ? formatSpeed(downloadSpeed) : "0 B/s"} {isGameDownloading ? formatSpeed(downloadSpeed) : "0 B/s"}
@@ -371,37 +373,38 @@ function HeroDownloadView({
<GraphIcon size={16} /> <GraphIcon size={16} />
</span> </span>
<div className="download-group__stat-content"> <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"> <span className="download-group__stat-value">
{peakSpeed > 0 ? formatSpeed(peakSpeed) : "0 B/s"} {peakSpeed > 0 ? formatSpeed(peakSpeed) : "0 B/s"}
</span> </span>
</div> </div>
</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 && ( {game.download?.downloader && (
<div className="download-group__stat-item"> <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> <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>
</div> </div>
)} )}
@@ -870,8 +873,14 @@ export function DownloadGroup({
<li key={game.id} className="download-group__simple-card"> <li key={game.id} className="download-group__simple-card">
<button <button
type="button" type="button"
onClick={() => navigate(buildGameDetailsPath(game))}
className="download-group__simple-thumbnail" 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} /> <img src={game.libraryImageUrl || ""} alt={game.title} />
</button> </button>
@@ -879,10 +888,18 @@ export function DownloadGroup({
<div className="download-group__simple-info"> <div className="download-group__simple-info">
<button <button
type="button" type="button"
className="download-group__simple-title"
onClick={() => navigate(buildGameDetailsPath(game))} onClick={() => navigate(buildGameDetailsPath(game))}
className="download-group__simple-title-button" style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
textAlign: "left",
width: "100%",
}}
> >
<h3 className="download-group__simple-title">{game.title}</h3> {game.title}
</button> </button>
<div className="download-group__simple-meta"> <div className="download-group__simple-meta">
<div className="download-group__simple-meta-row"> <div className="download-group__simple-meta-row">

View File

@@ -87,12 +87,16 @@ export function LibraryTab({
<ul className="profile-content__games-grid"> <ul className="profile-content__games-grid">
{pinnedGames?.map((game) => ( {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 <UserLibraryGameCard
game={game} game={game}
statIndex={statsIndex} statIndex={statsIndex}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
sortBy={sortBy} sortBy={sortBy}
/> />
</li> </li>
@@ -134,6 +138,9 @@ export function LibraryTab({
<motion.li <motion.li
key={`${sortBy}-${game.objectId}`} key={`${sortBy}-${game.objectId}`}
style={{ listStyle: "none" }} style={{ listStyle: "none" }}
className="user-library-game__wrapper"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
initial={ initial={
isNewGame isNewGame
? { opacity: 0.5, y: 15, scale: 0.96 } ? { opacity: 0.5, y: 15, scale: 0.96 }
@@ -160,8 +167,6 @@ export function LibraryTab({
<UserLibraryGameCard <UserLibraryGameCard
game={game} game={game}
statIndex={statsIndex} statIndex={statsIndex}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
sortBy={sortBy} sortBy={sortBy}
/> />
</motion.li> </motion.li>

View File

@@ -25,16 +25,12 @@ import "./user-library-game-card.scss";
interface UserLibraryGameCardProps { interface UserLibraryGameCardProps {
game: UserGame; game: UserGame;
statIndex: number; statIndex: number;
onMouseEnter: () => void;
onMouseLeave: () => void;
sortBy?: string; sortBy?: string;
} }
export function UserLibraryGameCard({ export function UserLibraryGameCard({
game, game,
statIndex, statIndex,
onMouseEnter,
onMouseLeave,
sortBy, sortBy,
}: UserLibraryGameCardProps) { }: UserLibraryGameCardProps) {
const { userProfile, isMe, getUserLibraryGames } = const { userProfile, isMe, getUserLibraryGames } =
@@ -130,129 +126,119 @@ export function UserLibraryGameCard({
return ( return (
<> <>
<li <button
onMouseEnter={onMouseEnter} type="button"
onMouseLeave={onMouseLeave} className="user-library-game__cover"
className="user-library-game__wrapper" onClick={() => navigate(buildUserGameDetailsPath(game))}
title={isTooltipHovered ? undefined : game.title} title={isTooltipHovered ? undefined : game.title}
> >
<button <div className="user-library-game__overlay">
type="button" {isMe && (
className="user-library-game__cover" <div className="user-library-game__actions-container">
onClick={() => navigate(buildUserGameDetailsPath(game))} <button
> type="button"
<div className="user-library-game__overlay"> className="user-library-game__pin-button"
{isMe && ( onClick={(e) => {
<div className="user-library-game__actions-container"> e.stopPropagation();
<button toggleGamePinned();
type="button" }}
className="user-library-game__pin-button" disabled={isPinning}
onClick={(e) => { >
e.stopPropagation(); {game.isPinned ? (
toggleGamePinned(); <PinSlashIcon size={12} />
}} ) : (
disabled={isPinning} <PinIcon size={12} />
> )}
{game.isPinned ? ( </button>
<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> </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 && {userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
game.achievementCount > 0 && ( <div className="user-library-game__stats">
<div className="user-library-game__stats"> <div className="user-library-game__stats-header">
<div className="user-library-game__stats-header"> <div className="user-library-game__stats-content">
<div className="user-library-game__stats-content"> <div
<div className="user-library-game__stats-item"
className="user-library-game__stats-item" style={{
style={{ transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`, }}
}} >
> <TrophyIcon size={13} />
<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>
<span> <span>
{formatDownloadProgress( {game.unlockedAchievementCount} / {game.achievementCount}
game.unlockedAchievementCount / game.achievementCount,
1
)}
</span> </span>
</div> </div>
<progress {game.achievementsPointsEarnedSum > 0 && (
max={1} <div
value={ className="user-library-game__stats-item"
game.unlockedAchievementCount / game.achievementCount style={{
} transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
className="user-library-game__achievements-progress" }}
/> >
<HydraIcon width={16} height={16} />
{formatAchievementPoints(
game.achievementsPointsEarnedSum
)}
</div>
)}
</div> </div>
)}
</div>
{imageError || !game.coverImageUrl ? ( <span>
<div className="user-library-game__cover-placeholder"> {formatDownloadProgress(
<ImageIcon size={48} /> game.unlockedAchievementCount / game.achievementCount,
1
)}
</span>
</div>
<progress
max={1}
value={game.unlockedAchievementCount / game.achievementCount}
className="user-library-game__achievements-progress"
/>
</div> </div>
) : (
<img
src={game.coverImageUrl}
alt={game.title}
className="user-library-game__game-image"
onError={() => setImageError(true)}
/>
)} )}
</button> </div>
</li>
{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 <Tooltip
id={game.objectId} id={game.objectId}
style={{ style={{

View File

@@ -20,8 +20,6 @@ export interface Auth {
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
tokenExpirationTimestamp: number; tokenExpirationTimestamp: number;
featurebaseJwt: string;
workwondersJwt: string;
} }
export interface User { export interface User {

View File

@@ -6330,7 +6330,7 @@ jsonwebtoken@^9.0.2:
object.assign "^4.1.4" object.assign "^4.1.4"
object.values "^1.1.6" object.values "^1.1.6"
jwa@^1.4.2: jwa@^1.4.1:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
@@ -6340,11 +6340,11 @@ jwa@^1.4.2:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
jws@^3.2.2: jws@^3.2.2:
version "3.2.3" version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g== integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies: dependencies:
jwa "^1.4.2" jwa "^1.4.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
keyv@^4.0.0, keyv@^4.5.3: keyv@^4.0.0, keyv@^4.5.3: