feat: improving library

This commit is contained in:
Chubby Granny Chaser
2025-11-10 22:20:44 +00:00
parent 65e2bb38a0
commit 46df34e8a5
12 changed files with 257 additions and 372 deletions

View File

@@ -719,9 +719,8 @@
"amount_minutes_short": "{{amount}}m", "amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "This playtime has been manually updated", "manual_playtime_tooltip": "This playtime has been manually updated",
"all_games": "All Games", "all_games": "All Games",
"favourited_games": "Favourited", "recently_played": "Recently Played",
"new_games": "New Games", "favorites": "Favorites"
"top_10": "Top 10"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Achievement unlocked", "achievement_unlocked": "Achievement unlocked",

View File

@@ -13,6 +13,7 @@
}, },
"sidebar": { "sidebar": {
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Librería",
"downloads": "Descargas", "downloads": "Descargas",
"settings": "Ajustes", "settings": "Ajustes",
"my_library": "Mi Librería", "my_library": "Mi Librería",
@@ -716,5 +717,26 @@
"hydra_cloud_feature_found": "¡Acabas de descubrir una característica de Hydra Cloud!", "hydra_cloud_feature_found": "¡Acabas de descubrir una característica de Hydra Cloud!",
"learn_more": "Descubrir más", "learn_more": "Descubrir más",
"debrid_description": "Descargas hasta x4 veces más rápidas con Nimbus" "debrid_description": "Descargas hasta x4 veces más rápidas con Nimbus"
},
"library": {
"library": "Librería",
"play": "Jugar",
"download": "Descargar",
"downloading": "Descargando",
"game": "juego",
"games": "juegos",
"grid_view": "Vista de cuadrícula",
"compact_view": "Vista compacta",
"large_view": "Vista grande",
"no_games_title": "Tu librería está vacía",
"no_games_description": "Agregá juegos del catálogo o descargalos para comenzar",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "Este tiempo de juego ha sido modificado manualmente",
"all_games": "Todos los Juegos",
"recently_played": "Jugados Recientemente",
"favorites": "Favoritos"
} }
} }

View File

@@ -13,6 +13,7 @@
}, },
"sidebar": { "sidebar": {
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Biblioteca",
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Ajustes", "settings": "Ajustes",
"my_library": "Biblioteca", "my_library": "Biblioteca",
@@ -731,5 +732,26 @@
"hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!", "hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!",
"learn_more": "Saiba mais", "learn_more": "Saiba mais",
"debrid_description": "Baixe até 4x mais rápido com Nimbus" "debrid_description": "Baixe até 4x mais rápido com Nimbus"
},
"library": {
"library": "Biblioteca",
"play": "Jogar",
"download": "Baixar",
"downloading": "Baixando",
"game": "jogo",
"games": "jogos",
"grid_view": "Visualização em grade",
"compact_view": "Visualização compacta",
"large_view": "Visualização grande",
"no_games_title": "Sua biblioteca está vazia",
"no_games_description": "Adicione jogos do catálogo ou baixe-os para começar",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
"all_games": "Todos os Jogos",
"recently_played": "Jogados Recentemente",
"favorites": "Favoritos"
} }
} }

View File

@@ -13,6 +13,7 @@
}, },
"sidebar": { "sidebar": {
"catalogue": "Каталог", "catalogue": "Каталог",
"library": "Библиотека",
"downloads": "Загрузки", "downloads": "Загрузки",
"settings": "Настройки", "settings": "Настройки",
"my_library": "Библиотека", "my_library": "Библиотека",
@@ -727,5 +728,26 @@
"hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!", "hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!",
"learn_more": "Подробнее", "learn_more": "Подробнее",
"debrid_description": "Скачивайте в 4 раза быстрее с Nimbus" "debrid_description": "Скачивайте в 4 раза быстрее с Nimbus"
},
"library": {
"library": "Библиотека",
"play": "Играть",
"download": "Скачать",
"downloading": "Скачивание",
"game": "игра",
"games": "игры",
"grid_view": "Вид сетки",
"compact_view": "Компактный вид",
"large_view": "Большой вид",
"no_games_title": "Ваша библиотека пуста",
"no_games_description": "Добавьте игры из каталога или скачайте их, чтобы начать",
"amount_hours": "{{amount}} часов",
"amount_minutes": "{{amount}} минут",
"amount_hours_short": "{{amount}}ч",
"amount_minutes_short": "{{amount}}м",
"manual_playtime_tooltip": "Время игры было обновлено вручную",
"all_games": "Все игры",
"recently_played": "Недавно сыгранные",
"favorites": "Избранное"
} }
} }

View File

@@ -1,63 +1,55 @@
@use "../../scss/globals.scss"; @use "../../scss/globals.scss";
.library-filter-options { .library-filter-options {
&__container { &__tabs {
display: flex; display: flex;
align-items: center;
gap: calc(globals.$spacing-unit); gap: calc(globals.$spacing-unit);
flex-wrap: wrap; position: relative;
} }
&__option { &__tab-wrapper {
display: flex; position: relative;
align-items: center; }
gap: calc(globals.$spacing-unit);
padding: 8px 12px; &__tab {
border-radius: 6px; background: none;
background: rgba(255, 255, 255, 0.05); border: none;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.6);
padding: calc(globals.$spacing-unit) calc(globals.$spacing-unit * 2);
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 14px;
font-weight: 500; font-weight: 500;
transition: all ease 0.2s; transition: color ease 0.2s;
white-space: nowrap; /* prevent label and count from wrapping */ display: flex;
border: 1px solid rgba(0, 0, 0, 0.06); align-items: center;
gap: calc(globals.$spacing-unit * 0.5);
&:hover { &--active {
color: rgba(255, 255, 255, 0.9); color: white;
background: rgba(255, 255, 255, 0.08);
}
&.active {
color: #000;
background: #fff;
svg,
svg * {
fill: currentColor;
color: currentColor;
}
.library-filter-options__count {
background: #ebebeb;
color: rgba(0, 0, 0, 0.9);
}
} }
} }
&__label { &__tab-badge {
font-weight: 500; display: inline-flex;
white-space: nowrap; align-items: center;
} justify-content: center;
min-width: 18px;
&__count { height: 18px;
background: rgba(255, 255, 255, 0.16); padding: 0 6px;
color: rgba(255, 255, 255, 0.95); background-color: rgba(255, 255, 255, 0.15);
padding: 2px 8px; border-radius: 9px;
border-radius: 4px; font-size: 11px;
font-size: 12px;
font-weight: 600; font-weight: 600;
min-width: 24px; color: rgba(255, 255, 255, 0.9);
text-align: center; line-height: 1;
transition: all ease 0.2s; }
&__tab-underline {
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: white;
} }
} }

View File

@@ -1,61 +1,103 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "./filter-options.scss"; import "./filter-options.scss";
export type FilterOption = "all" | "favourited" | "new" | "top10"; export type FilterOption = "all" | "recently_played" | "favorites";
interface FilterOptionsProps { interface FilterOptionsProps {
filterBy: FilterOption; filterBy: FilterOption;
onFilterChange: (filterBy: FilterOption) => void; onFilterChange: (filterBy: FilterOption) => void;
allGamesCount: number; allGamesCount: number;
favouritedCount: number; recentlyPlayedCount: number;
newGamesCount: number; favoritesCount: number;
top10Count: number;
} }
export function FilterOptions({ export function FilterOptions({
filterBy, filterBy,
onFilterChange, onFilterChange,
allGamesCount, allGamesCount,
favouritedCount, recentlyPlayedCount,
newGamesCount, favoritesCount,
top10Count,
}: Readonly<FilterOptionsProps>) { }: Readonly<FilterOptionsProps>) {
const { t } = useTranslation("library"); const { t } = useTranslation("library");
return ( return (
<div className="library-filter-options__container"> <div className="library-filter-options__tabs">
<button <div className="library-filter-options__tab-wrapper">
className={`library-filter-options__option ${filterBy === "all" ? "active" : ""}`} <button
onClick={() => onFilterChange("all")} type="button"
> className={`library-filter-options__tab ${filterBy === "all" ? "library-filter-options__tab--active" : ""}`}
<span className="library-filter-options__label">{t("all_games")}</span> onClick={() => onFilterChange("all")}
<span className="library-filter-options__count">{allGamesCount}</span> >
</button> {t("all_games")}
<button {allGamesCount > 0 && (
className={`library-filter-options__option ${filterBy === "favourited" ? "active" : ""}`} <span className="library-filter-options__tab-badge">
onClick={() => onFilterChange("favourited")} {allGamesCount}
> </span>
<span className="library-filter-options__label"> )}
{t("Favourite Games")} </button>
</span> {filterBy === "all" && (
<span className="library-filter-options__count">{favouritedCount}</span> <motion.div
</button> className="library-filter-options__tab-underline"
<button layoutId="library-tab-underline"
className={`library-filter-options__option ${filterBy === "new" ? "active" : ""}`} transition={{
onClick={() => onFilterChange("new")} type: "spring",
> stiffness: 300,
<span className="library-filter-options__label">{t("new_games")}</span> damping: 30,
<span className="library-filter-options__count">{newGamesCount}</span> }}
</button> />
<button )}
className={`library-filter-options__option ${filterBy === "top10" ? "active" : ""}`} </div>
onClick={() => onFilterChange("top10")} <div className="library-filter-options__tab-wrapper">
> <button
<span className="library-filter-options__label"> type="button"
{t("Most Played")} className={`library-filter-options__tab ${filterBy === "recently_played" ? "library-filter-options__tab--active" : ""}`}
</span> onClick={() => onFilterChange("recently_played")}
<span className="library-filter-options__count">{top10Count}</span> >
</button> {t("recently_played")}
{recentlyPlayedCount > 0 && (
<span className="library-filter-options__tab-badge">
{recentlyPlayedCount}
</span>
)}
</button>
{filterBy === "recently_played" && (
<motion.div
className="library-filter-options__tab-underline"
layoutId="library-tab-underline"
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
/>
)}
</div>
<div className="library-filter-options__tab-wrapper">
<button
type="button"
className={`library-filter-options__tab ${filterBy === "favorites" ? "library-filter-options__tab--active" : ""}`}
onClick={() => onFilterChange("favorites")}
>
{t("favorites")}
{favoritesCount > 0 && (
<span className="library-filter-options__tab-badge">
{favoritesCount}
</span>
)}
</button>
{filterBy === "favorites" && (
<motion.div
className="library-filter-options__tab-underline"
layoutId="library-tab-underline"
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
/>
)}
</div>
</div> </div>
); );
} }

View File

@@ -84,36 +84,6 @@
gap: calc(globals.$spacing-unit); gap: calc(globals.$spacing-unit);
} }
&__menu-button {
align-self: flex-start;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: solid 1px rgba(255, 255, 255, 0.15);
border-radius: 4px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all ease 0.2s;
color: rgba(255, 255, 255, 0.95);
padding: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
opacity: 0;
transform: scale(0.9);
&:hover {
background: rgba(0, 0, 0, 0.6);
border-color: rgba(255, 255, 255, 0.25);
transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
}
&__logo-container { &__logo-container {
flex: 1; flex: 1;
@@ -238,50 +208,4 @@
white-space: nowrap; white-space: nowrap;
} }
&__action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all ease 0.2s;
flex: 0 0 auto;
&:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
&:active {
transform: scale(0.98);
}
}
&:hover &__menu-button {
opacity: 1;
transform: scale(1);
}
&__action-icon--downloading {
animation: pulse 1.5s ease-in-out infinite;
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
} }

View File

@@ -1,18 +1,11 @@
import { LibraryGame } from "@types"; import { LibraryGame } from "@types";
import { useDownload, useGameCard } from "@renderer/hooks"; import { useGameCard } from "@renderer/hooks";
import { import {
PlayIcon,
DownloadIcon,
ClockIcon, ClockIcon,
AlertFillIcon, AlertFillIcon,
ThreeBarsIcon,
TrophyIcon, TrophyIcon,
XIcon,
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
import { useGameActions } from "@renderer/components/game-context-menu/use-game-actions";
import { logger } from "@renderer/logger";
import "./library-game-card-large.scss"; import "./library-game-card-large.scss";
interface LibraryGameCardLargeProps { interface LibraryGameCardLargeProps {
@@ -35,48 +28,12 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
game, game,
onContextMenu, onContextMenu,
}: Readonly<LibraryGameCardLargeProps>) { }: Readonly<LibraryGameCardLargeProps>) {
const { t } = useTranslation("library");
const { lastPacket } = useDownload();
const { const {
formatPlayTime, formatPlayTime,
handleCardClick, handleCardClick,
handleContextMenuClick, handleContextMenuClick,
handleMenuButtonClick,
} = useGameCard(game, onContextMenu); } = useGameCard(game, onContextMenu);
const isGameDownloading =
game?.download?.status === "active" && lastPacket?.gameId === game?.id;
const {
handlePlayGame,
handleOpenDownloadOptions,
handleCloseGame,
isGameRunning,
} = useGameActions(game);
const handleActionClick = async (e: React.MouseEvent) => {
e.stopPropagation();
if (isGameRunning) {
try {
await handleCloseGame();
} catch (e) {
logger.error(e);
}
return;
}
try {
await handlePlayGame();
} catch (err) {
logger.error(err);
try {
handleOpenDownloadOptions();
} catch (e) {
logger.error(e);
}
}
};
const backgroundImage = useMemo( const backgroundImage = useMemo(
() => () =>
getImageWithCustomPriority( getImageWithCustomPriority(
@@ -129,14 +86,6 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
{formatPlayTime(game.playTimeInMilliseconds)} {formatPlayTime(game.playTimeInMilliseconds)}
</span> </span>
</div> </div>
<button
type="button"
className="library-game-card-large__menu-button"
onClick={handleMenuButtonClick}
title="More options"
>
<ThreeBarsIcon size={16} />
</button>
</div> </div>
<div className="library-game-card-large__logo-container"> <div className="library-game-card-large__logo-container">
@@ -183,51 +132,6 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
</div> </div>
</div> </div>
)} )}
<button
type="button"
className="library-game-card-large__action-button"
onClick={handleActionClick}
>
{(() => {
if (isGameDownloading) {
return (
<>
<DownloadIcon
size={16}
className="library-game-card-large__action-icon--downloading"
/>
{t("downloading")}
</>
);
}
if (isGameRunning) {
return (
<>
<XIcon size={16} />
{t("close")}
</>
);
}
if (game.executablePath) {
return (
<>
<PlayIcon size={16} />
{t("play")}
</>
);
}
return (
<>
<DownloadIcon size={16} />
{t("download")}
</>
);
})()}
</button>
</div> </div>
</div> </div>
</button> </button>

View File

@@ -109,10 +109,10 @@
&__achievements { &__achievements {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
opacity: 0; opacity: 1;
transform: translateY(8px); transform: translateY(0);
transition: all ease 0.2s; transition: all ease 0.2s;
pointer-events: none; pointer-events: auto;
width: 100%; width: 100%;
} }
@@ -204,53 +204,12 @@
} }
} }
&__menu-button { &__wrapper:hover &__action-button {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: solid 1px rgba(255, 255, 255, 0.15);
border-radius: 4px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all ease 0.2s;
color: rgba(255, 255, 255, 0.8);
padding: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
opacity: 0;
transform: scale(0.9);
&:hover {
background: rgba(0, 0, 0, 0.6);
border-color: rgba(255, 255, 255, 0.25);
transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
}
&__wrapper:hover &__action-button,
&__wrapper:hover &__menu-button {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
&__wrapper:hover &__achievements {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
&__action-icon {
&--downloading {
animation: pulse 1.5s ease-in-out infinite;
}
}
&__game-image { &__game-image {
object-fit: cover; object-fit: cover;

View File

@@ -4,7 +4,6 @@ import { memo } from "react";
import { import {
ClockIcon, ClockIcon,
AlertFillIcon, AlertFillIcon,
ThreeBarsIcon,
TrophyIcon, TrophyIcon,
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import "./library-game-card.scss"; import "./library-game-card.scss";
@@ -31,7 +30,6 @@ export const LibraryGameCard = memo(function LibraryGameCard({
formatPlayTime, formatPlayTime,
handleCardClick, handleCardClick,
handleContextMenuClick, handleContextMenuClick,
handleMenuButtonClick,
} = useGameCard(game, onContextMenu); } = useGameCard(game, onContextMenu);
const coverImage = const coverImage =
@@ -69,18 +67,8 @@ export const LibraryGameCard = memo(function LibraryGameCard({
{formatPlayTime(game.playTimeInMilliseconds, true)} {formatPlayTime(game.playTimeInMilliseconds, true)}
</span> </span>
</div> </div>
<button
type="button"
className="library-game-card__menu-button"
onClick={handleMenuButtonClick}
title="More options"
>
<ThreeBarsIcon size={16} />
</button>
</div> </div>
{/* Achievements section - shown on hover */}
{(game.achievementCount ?? 0) > 0 && ( {(game.achievementCount ?? 0) > 0 && (
<div className="library-game-card__achievements"> <div className="library-game-card__achievements">
<div className="library-game-card__achievement-header"> <div className="library-game-card__achievement-header">

View File

@@ -38,12 +38,17 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
position: relative;
} }
&__controls-left { &__controls-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: calc(globals.$spacing-unit); gap: calc(globals.$spacing-unit);
flex: 1;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
margin-right: calc(globals.$spacing-unit * 2);
} }
&__controls-right { &__controls-right {

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState, useCallback } from "react"; import { useEffect, useMemo, useState, useCallback } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { useLibrary, useAppDispatch, useAppSelector } from "@renderer/hooks"; import { useLibrary, useAppDispatch, useAppSelector } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features"; import { setHeaderTitle } from "@renderer/features";
import { TelescopeIcon } from "@primer/octicons-react"; import { TelescopeIcon } from "@primer/octicons-react";
@@ -77,23 +78,12 @@ export default function Library() {
let filtered; let filtered;
switch (filterBy) { switch (filterBy) {
case "favourited": case "recently_played":
filtered = library.filter((game) => game.lastTimePlayed !== null);
break;
case "favorites":
filtered = library.filter((game) => game.favorite); filtered = library.filter((game) => game.favorite);
break; break;
case "new":
filtered = library.filter(
(game) => (game.playTimeInMilliseconds || 0) === 0
);
break;
case "top10":
filtered = library
.slice()
.sort(
(a, b) =>
(b.playTimeInMilliseconds || 0) - (a.playTimeInMilliseconds || 0)
)
.slice(0, 10);
break;
case "all": case "all":
default: default:
filtered = library; filtered = library;
@@ -124,19 +114,18 @@ export default function Library() {
const filterCounts = useMemo(() => { const filterCounts = useMemo(() => {
const allGamesCount = library.length; const allGamesCount = library.length;
let favouritedCount = 0; let recentlyPlayedCount = 0;
let newGamesCount = 0; let favoritesCount = 0;
for (const game of library) { for (const game of library) {
if (game.favorite) favouritedCount++; if (game.lastTimePlayed !== null) recentlyPlayedCount++;
if ((game.playTimeInMilliseconds || 0) === 0) newGamesCount++; if (game.favorite) favoritesCount++;
} }
return { return {
allGamesCount, allGamesCount,
favouritedCount, recentlyPlayedCount,
newGamesCount, favoritesCount,
top10Count: Math.min(10, allGamesCount),
}; };
}, [library]); }, [library]);
@@ -152,9 +141,8 @@ export default function Library() {
filterBy={filterBy} filterBy={filterBy}
onFilterChange={setFilterBy} onFilterChange={setFilterBy}
allGamesCount={filterCounts.allGamesCount} allGamesCount={filterCounts.allGamesCount}
favouritedCount={filterCounts.favouritedCount} recentlyPlayedCount={filterCounts.recentlyPlayedCount}
newGamesCount={filterCounts.newGamesCount} favoritesCount={filterCounts.favoritesCount}
top10Count={filterCounts.top10Count}
/> />
</div> </div>
@@ -175,34 +163,52 @@ export default function Library() {
</div> </div>
)} )}
{hasGames && viewMode === "large" && ( {hasGames && (
<div className="library__games-list library__games-list--large"> <AnimatePresence mode="wait">
{sortedLibrary.map((game) => ( {viewMode === "large" && (
<LibraryGameCardLarge <motion.div
key={`${game.shop}-${game.objectId}`} key={`${filterBy}-large`}
game={game} className="library__games-list library__games-list--large"
onContextMenu={handleOpenContextMenu} initial={{ opacity: 0, x: -10 }}
/> animate={{ opacity: 1, x: 0 }}
))} exit={{ opacity: 0, x: 10 }}
</div> transition={{ duration: 0.2 }}
)}
{hasGames && viewMode !== "large" && (
<ul className={`library__games-grid library__games-grid--${viewMode}`}>
{sortedLibrary.map((game) => (
<li
key={`${game.shop}-${game.objectId}`}
style={{ listStyle: "none" }}
> >
<LibraryGameCard {sortedLibrary.map((game) => (
game={game} <LibraryGameCardLarge
onMouseEnter={handleOnMouseEnterGameCard} key={`${game.shop}-${game.objectId}`}
onMouseLeave={handleOnMouseLeaveGameCard} game={game}
onContextMenu={handleOpenContextMenu} onContextMenu={handleOpenContextMenu}
/> />
</li> ))}
))} </motion.div>
</ul> )}
{viewMode !== "large" && (
<motion.ul
key={`${filterBy}-${viewMode}`}
className={`library__games-grid library__games-grid--${viewMode}`}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.2 }}
>
{sortedLibrary.map((game) => (
<li
key={`${game.shop}-${game.objectId}`}
style={{ listStyle: "none" }}
>
<LibraryGameCard
game={game}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
onContextMenu={handleOpenContextMenu}
/>
</li>
))}
</motion.ul>
)}
</AnimatePresence>
)} )}
{contextMenu.game && ( {contextMenu.game && (