From 3bef0c92695b31f54e0cba8d14dd5f75bd2ce5f1 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 6 Nov 2025 18:26:56 +0200 Subject: [PATCH 1/7] feat: library ui changes and searchbar removal --- src/locales/en/translation.json | 1 + src/renderer/src/app.scss | 3 +- .../src/components/header/header.scss | 4 +- src/renderer/src/components/header/header.tsx | 39 +++++++--- src/renderer/src/features/library-slice.ts | 7 +- .../src/pages/library/filter-options.scss | 4 +- .../library/library-game-card-large.scss | 24 ++---- .../src/pages/library/library-game-card.scss | 39 +++++----- .../src/pages/library/library-game-card.tsx | 2 +- src/renderer/src/pages/library/library.scss | 20 ++--- src/renderer/src/pages/library/library.tsx | 6 +- .../src/pages/library/search-bar.scss | 75 ------------------- src/renderer/src/pages/library/search-bar.tsx | 44 ----------- .../src/pages/library/view-options.scss | 6 +- 14 files changed, 82 insertions(+), 192 deletions(-) delete mode 100644 src/renderer/src/pages/library/search-bar.scss delete mode 100644 src/renderer/src/pages/library/search-bar.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9989f153..cb2473eb 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -93,6 +93,7 @@ }, "header": { "search": "Search games", + "search_library": "Search library", "home": "Home", "catalogue": "Catalogue", "library": "Library", diff --git a/src/renderer/src/app.scss b/src/renderer/src/app.scss index ed7b9aa8..0d992553 100644 --- a/src/renderer/src/app.scss +++ b/src/renderer/src/app.scss @@ -5,7 +5,7 @@ } ::-webkit-scrollbar { - width: 4px; + width: 9px; background-color: globals.$dark-background-color; } @@ -90,6 +90,7 @@ img { progress[value] { -webkit-appearance: none; + appearance: none; } .container { diff --git a/src/renderer/src/components/header/header.scss b/src/renderer/src/components/header/header.scss index cd25d8e2..f0c72ce0 100644 --- a/src/renderer/src/components/header/header.scss +++ b/src/renderer/src/components/header/header.scss @@ -24,7 +24,7 @@ background-color: globals.$background-color; display: inline-flex; transition: all ease 0.2s; - width: 300px; + width: 200px; align-items: center; border-radius: 8px; border: solid 1px globals.$border-color; @@ -35,7 +35,7 @@ } &--focused { - width: 350px; + width: 250px; border-color: #dadbe1; } } diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 0f452bf2..d3164ced 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -7,7 +7,7 @@ import { useAppDispatch, useAppSelector } from "@renderer/hooks"; import "./header.scss"; import { AutoUpdateSubHeader } from "./auto-update-sub-header"; -import { setFilters } from "@renderer/features"; +import { setFilters, setLibrarySearchQuery } from "@renderer/features"; import cn from "classnames"; const pathTitle: Record = { @@ -28,10 +28,20 @@ export function Header() { (state) => state.window ); - const searchValue = useAppSelector( + const catalogueSearchValue = useAppSelector( (state) => state.catalogueSearch.filters.title ); + const librarySearchValue = useAppSelector( + (state) => state.library.searchQuery + ); + + const isOnLibraryPage = location.pathname.startsWith("/library"); + + const searchValue = isOnLibraryPage + ? librarySearchValue + : catalogueSearchValue; + const dispatch = useAppDispatch(); const [isFocused, setIsFocused] = useState(false); @@ -63,18 +73,29 @@ export function Header() { }; const handleSearch = (value: string) => { - dispatch(setFilters({ title: value.slice(0, 255) })); + if (isOnLibraryPage) { + dispatch(setLibrarySearchQuery(value.slice(0, 255))); + } else { + dispatch(setFilters({ title: value.slice(0, 255) })); + if (!location.pathname.startsWith("/catalogue")) { + navigate("/catalogue"); + } + } + }; - if (!location.pathname.startsWith("/catalogue")) { - navigate("/catalogue"); + const handleClearSearch = () => { + if (isOnLibraryPage) { + dispatch(setLibrarySearchQuery("")); + } else { + dispatch(setFilters({ title: "" })); } }; useEffect(() => { - if (!location.pathname.startsWith("/catalogue") && searchValue) { + if (!location.pathname.startsWith("/catalogue") && catalogueSearchValue) { dispatch(setFilters({ title: "" })); } - }, [location.pathname, searchValue, dispatch]); + }, [location.pathname, catalogueSearchValue, dispatch]); return ( <> @@ -123,7 +144,7 @@ export function Header() { ref={inputRef} type="text" name="search" - placeholder={t("search")} + placeholder={isOnLibraryPage ? t("search_library") : t("search")} value={searchValue} className="header__search-input" onChange={(event) => handleSearch(event.target.value)} @@ -134,7 +155,7 @@ export function Header() { {searchValue && ( - )} - - - ); -}; diff --git a/src/renderer/src/pages/library/view-options.scss b/src/renderer/src/pages/library/view-options.scss index 77bfc10e..13307864 100644 --- a/src/renderer/src/pages/library/view-options.scss +++ b/src/renderer/src/pages/library/view-options.scss @@ -26,7 +26,7 @@ display: flex; align-items: center; gap: calc(globals.$spacing-unit); - padding: 8px 10px; + padding: 10px; border-radius: 6px; background: rgba(255, 255, 255, 0.04); border: none; @@ -38,8 +38,8 @@ white-space: nowrap; &:hover { - color: rgba(255, 255, 255, 0.95); - background: rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.08); } &.active { From c3a4990a507cca27396e5dbe537e898e66df22dc Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 8 Nov 2025 14:28:54 +0200 Subject: [PATCH 2/7] ci: performance optimizing in library --- .../pages/library/library-game-card-large.tsx | 330 +++++++++--------- .../src/pages/library/library-game-card.tsx | 234 ++++++------- src/renderer/src/pages/library/library.tsx | 131 +++++-- 3 files changed, 369 insertions(+), 326 deletions(-) diff --git a/src/renderer/src/pages/library/library-game-card-large.tsx b/src/renderer/src/pages/library/library-game-card-large.tsx index 5628fe10..4cb54977 100644 --- a/src/renderer/src/pages/library/library-game-card-large.tsx +++ b/src/renderer/src/pages/library/library-game-card-large.tsx @@ -12,14 +12,18 @@ import { XIcon, } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; -import { useCallback, useState } from "react"; +import { useCallback, memo, useMemo } from "react"; import { useGameActions } from "@renderer/components/game-context-menu/use-game-actions"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; -import { GameContextMenu } from "@renderer/components"; +import { logger } from "@renderer/logger"; import "./library-game-card-large.scss"; interface LibraryGameCardLargeProps { game: LibraryGame; + onContextMenu: ( + game: LibraryGame, + position: { x: number; y: number } + ) => void; } const getImageWithCustomPriority = ( @@ -30,17 +34,14 @@ const getImageWithCustomPriority = ( return customUrl || originalUrl || fallbackUrl || ""; }; -export function LibraryGameCardLarge({ +export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({ game, + onContextMenu, }: Readonly) { const { t } = useTranslation("library"); const { numberFormatter } = useFormat(); const navigate = useNavigate(); const { lastPacket } = useDownload(); - const [contextMenu, setContextMenu] = useState<{ - visible: boolean; - position: { x: number; y: number }; - }>({ visible: false, position: { x: 0, y: 0 } }); const isGameDownloading = game?.download?.status === "active" && lastPacket?.gameId === game?.id; @@ -84,196 +85,193 @@ export function LibraryGameCardLarge({ try { await handleCloseGame(); } catch (e) { - console.error(e); + logger.error(e); } return; } try { await handlePlayGame(); } catch (err) { - console.error(err); + logger.error(err); try { handleOpenDownloadOptions(); } catch (e) { - console.error(e); + logger.error(e); } } }; - const handleContextMenu = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - setContextMenu({ - visible: true, - position: { x: e.clientX, y: e.clientY }, - }); - }; - - const handleMenuButtonClick = (e: React.MouseEvent) => { - e.stopPropagation(); - setContextMenu({ - visible: true, - position: { - x: e.currentTarget.getBoundingClientRect().right, - y: e.currentTarget.getBoundingClientRect().bottom, - }, - }); - }; - - const handleCloseContextMenu = () => { - setContextMenu({ visible: false, position: { x: 0, y: 0 } }); - }; - - // Use libraryHeroImageUrl as background, fallback to libraryImageUrl - const backgroundImage = getImageWithCustomPriority( - game.libraryHeroImageUrl, - game.libraryImageUrl, - game.iconUrl + const handleContextMenuClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + onContextMenu(game, { x: e.clientX, y: e.clientY }); + }, + [game, onContextMenu] + ); + + const handleMenuButtonClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + const rect = e.currentTarget.getBoundingClientRect(); + onContextMenu(game, { x: rect.right, y: rect.bottom }); + }, + [game, onContextMenu] + ); + + const backgroundImage = useMemo( + () => + getImageWithCustomPriority( + game.libraryHeroImageUrl, + game.libraryImageUrl, + game.iconUrl + ), + [game.libraryHeroImageUrl, game.libraryImageUrl, game.iconUrl] + ); + + const backgroundStyle = useMemo( + () => ({ backgroundImage: `url(${backgroundImage})` }), + [backgroundImage] + ); + + const achievementBarStyle = useMemo( + () => ({ + width: `${((game.unlockedAchievementCount ?? 0) / (game.achievementCount ?? 1)) * 100}%`, + }), + [game.unlockedAchievementCount, game.achievementCount] ); - // For logo, check if logoImageUrl exists (similar to game details page) const logoImage = game.logoImageUrl; return ( - <> - - - -
- {logoImage ? ( - {game.title} +
+
+ {game.hasManuallyUpdatedPlaytime ? ( + ) : ( -

{game.title}

+ )} + + {formatPlayTime(game.playTimeInMilliseconds)} +
+ +
-
- {/* Achievements section */} - {(game.achievementCount ?? 0) > 0 && ( -
-
-
- - - {game.unlockedAchievementCount ?? 0} /{" "} - {game.achievementCount ?? 0} - -
- - {Math.round( - ((game.unlockedAchievementCount ?? 0) / - (game.achievementCount ?? 1)) * - 100 - )} - % +
+ {logoImage ? ( + {game.title} + ) : ( +

{game.title}

+ )} +
+ +
+ {/* Achievements section */} + {(game.achievementCount ?? 0) > 0 && ( +
+
+
+ + + {game.unlockedAchievementCount ?? 0} /{" "} + {game.achievementCount ?? 0}
-
-
-
+ + {Math.round( + ((game.unlockedAchievementCount ?? 0) / + (game.achievementCount ?? 1)) * + 100 + )} + % +
- )} - - -
+ } + + if (isGameRunning) { + return ( + <> + + {t("close")} + + ); + } + + if (game.executablePath) { + return ( + <> + + {t("play")} + + ); + } + + return ( + <> + + {t("download")} + + ); + })()} +
- - - +
+ ); -} +}); diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx index 7dbdff95..1b5b7afa 100644 --- a/src/renderer/src/pages/library/library-game-card.tsx +++ b/src/renderer/src/pages/library/library-game-card.tsx @@ -1,7 +1,7 @@ import { LibraryGame } from "@types"; import { useFormat } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { useCallback, useState } from "react"; +import { useCallback, memo } from "react"; import { buildGameDetailsPath } from "@renderer/helpers"; import { ClockIcon, @@ -10,30 +10,30 @@ import { TrophyIcon, } from "@primer/octicons-react"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; -import { Tooltip } from "react-tooltip"; import { useTranslation } from "react-i18next"; -import { GameContextMenu } from "@renderer/components"; 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 function LibraryGameCard({ +export const LibraryGameCard = memo(function LibraryGameCard({ game, onMouseEnter, onMouseLeave, + onContextMenu, }: Readonly) { const { t } = useTranslation("library"); const { numberFormatter } = useFormat(); const navigate = useNavigate(); - const [isTooltipHovered, setIsTooltipHovered] = useState(false); - const [contextMenu, setContextMenu] = useState<{ - visible: boolean; - position: { x: number; y: number }; - }>({ visible: false, position: { x: 0, y: 0 } }); const formatPlayTime = useCallback( (playTimeInMilliseconds = 0, isShort = false) => { @@ -60,30 +60,23 @@ export function LibraryGameCard({ navigate(buildGameDetailsPath(game)); }; - const handleContextMenu = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleContextMenuClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + onContextMenu(game, { x: e.clientX, y: e.clientY }); + }, + [game, onContextMenu] + ); - setContextMenu({ - visible: true, - position: { x: e.clientX, y: e.clientY }, - }); - }; - - const handleMenuButtonClick = (e: React.MouseEvent) => { - e.stopPropagation(); - setContextMenu({ - visible: true, - position: { - x: e.currentTarget.getBoundingClientRect().right, - y: e.currentTarget.getBoundingClientRect().bottom, - }, - }); - }; - - const handleCloseContextMenu = () => { - setContextMenu({ visible: false, position: { x: 0, y: 0 } }); - }; + const handleMenuButtonClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + const rect = e.currentTarget.getBoundingClientRect(); + onContextMenu(game, { x: rect.right, y: rect.bottom }); + }, + [game, onContextMenu] + ); const coverImage = game.coverImageUrl ?? @@ -93,110 +86,85 @@ export function LibraryGameCard({ undefined; return ( - <> - +
- {game.title} - - setIsTooltipHovered(true)} - afterHide={() => setIsTooltipHovered(false)} + {/* Achievements section - shown on hover */} + {(game.achievementCount ?? 0) > 0 && ( +
+
+
+ + + {game.unlockedAchievementCount ?? 0} /{" "} + {game.achievementCount ?? 0} + +
+ + {Math.round( + ((game.unlockedAchievementCount ?? 0) / + (game.achievementCount ?? 1)) * + 100 + )} + % + +
+
+
+
+
+ )} +
+ + {game.title} - - + ); -} +}); diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx index 8b4ad6a9..4f1e7ed2 100644 --- a/src/renderer/src/pages/library/library.tsx +++ b/src/renderer/src/pages/library/library.tsx @@ -1,10 +1,12 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useCallback, useRef } from "react"; import { useLibrary, useAppDispatch, useAppSelector } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; import { TelescopeIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; +import { LibraryGame } from "@types"; +import { GameContextMenu } from "@renderer/components"; +import VirtualList from "rc-virtual-list"; import { LibraryGameCard } from "./library-game-card"; -// detailed view removed — keep file if needed later import { LibraryGameCardLarge } from "./library-game-card-large"; import { ViewOptions, ViewMode } from "./view-options"; import { FilterOptions, FilterOption } from "./filter-options"; @@ -19,6 +21,14 @@ export default function Library() { const [viewMode, setViewMode] = useState("compact"); const [filterBy, setFilterBy] = useState("all"); + const [containerHeight, setContainerHeight] = useState(800); + const [contextMenu, setContextMenu] = useState<{ + game: LibraryGame | null; + visible: boolean; + position: { x: number; y: number }; + }>({ game: null, visible: false, position: { x: 0, y: 0 } }); + + const containerRef = useRef(null); const searchQuery = useAppSelector((state) => state.library.searchQuery); const dispatch = useAppDispatch(); const { t } = useTranslation("library"); @@ -47,13 +57,37 @@ export default function Library() { }; }, [dispatch, t, updateLibrary]); - const handleOnMouseEnterGameCard = () => { - // Optional: pause animations if needed - }; + useEffect(() => { + const updateHeight = () => { + if (containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + setContainerHeight(window.innerHeight - rect.top); + } + }; - const handleOnMouseLeaveGameCard = () => { + updateHeight(); + window.addEventListener("resize", updateHeight); + return () => window.removeEventListener("resize", updateHeight); + }, []); + + const handleOnMouseEnterGameCard = useCallback(() => { + // Optional: pause animations if needed + }, []); + + const handleOnMouseLeaveGameCard = useCallback(() => { // Optional: resume animations if needed - }; + }, []); + + const handleOpenContextMenu = useCallback( + (game: LibraryGame, position: { x: number; y: number }) => { + setContextMenu({ game, visible: true, position }); + }, + [] + ); + + const handleCloseContextMenu = useCallback(() => { + setContextMenu({ game: null, visible: false, position: { x: 0, y: 0 } }); + }, []); const filteredLibrary = useMemo(() => { let filtered; @@ -102,21 +136,33 @@ export default function Library() { }); }, [library, filterBy, searchQuery]); - // No sorting for now — rely on filteredLibrary const sortedLibrary = filteredLibrary; - // Calculate counts for filters - const allGamesCount = library.length; - const favouritedCount = library.filter((game) => game.favorite).length; - const newGamesCount = library.filter( - (game) => (game.playTimeInMilliseconds || 0) === 0 - ).length; - const top10Count = Math.min(10, library.length); + const filterCounts = useMemo(() => { + const allGamesCount = library.length; + let favouritedCount = 0; + let newGamesCount = 0; + + for (const game of library) { + if (game.favorite) favouritedCount++; + if ((game.playTimeInMilliseconds || 0) === 0) newGamesCount++; + } + + return { + allGamesCount, + favouritedCount, + newGamesCount, + top10Count: Math.min(10, allGamesCount), + }; + }, [library]); const hasGames = library.length > 0; + const itemHeight = + viewMode === "large" ? 200 : viewMode === "grid" ? 240 : 180; + return ( -
+
{hasGames && (
@@ -124,10 +170,10 @@ export default function Library() {
@@ -148,17 +194,38 @@ export default function Library() {
)} - {hasGames && viewMode === "large" && ( + {hasGames && sortedLibrary.length > 50 && viewMode === "large" && (
- {sortedLibrary.map((game) => ( - - ))} + `${game.shop}-${game.objectId}`} + > + {(game) => ( + + )} +
)} + {hasGames && + (sortedLibrary.length <= 50 || viewMode !== "large") && + viewMode === "large" && ( +
+ {sortedLibrary.map((game) => ( + + ))} +
+ )} + {hasGames && viewMode !== "large" && (
    {sortedLibrary.map((game) => ( @@ -170,11 +237,21 @@ export default function Library() { game={game} onMouseEnter={handleOnMouseEnterGameCard} onMouseLeave={handleOnMouseLeaveGameCard} + onContextMenu={handleOpenContextMenu} /> ))}
)} + + {contextMenu.game && ( + + )}
); } From 196413ee280ffbfdd14ffe3adaf2f1ff83562e97 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 8 Nov 2025 14:48:42 +0200 Subject: [PATCH 3/7] fix: duplicated lines --- src/renderer/src/hooks/index.ts | 1 + src/renderer/src/hooks/use-game-card.ts | 66 +++++++++++++++++++ .../pages/library/library-game-card-large.tsx | 58 +++------------- .../src/pages/library/library-game-card.tsx | 60 +++-------------- 4 files changed, 83 insertions(+), 102 deletions(-) create mode 100644 src/renderer/src/hooks/use-game-card.ts diff --git a/src/renderer/src/hooks/index.ts b/src/renderer/src/hooks/index.ts index 73733e2b..23190def 100644 --- a/src/renderer/src/hooks/index.ts +++ b/src/renderer/src/hooks/index.ts @@ -6,3 +6,4 @@ export * from "./redux"; export * from "./use-user-details"; export * from "./use-format"; export * from "./use-feature"; +export * from "./use-game-card"; diff --git a/src/renderer/src/hooks/use-game-card.ts b/src/renderer/src/hooks/use-game-card.ts new file mode 100644 index 00000000..98987189 --- /dev/null +++ b/src/renderer/src/hooks/use-game-card.ts @@ -0,0 +1,66 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { useFormat } from "./use-format"; +import { useTranslation } from "react-i18next"; +import { buildGameDetailsPath } from "@renderer/helpers"; +import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; +import { LibraryGame } from "@types"; + +export function useGameCard( + game: LibraryGame, + onContextMenu: (game: LibraryGame, position: { x: number; y: number }) => void +) { + const { t } = useTranslation("library"); + const { numberFormatter } = useFormat(); + const navigate = useNavigate(); + + const formatPlayTime = useCallback( + (playTimeInMilliseconds = 0, isShort = false) => { + const minutes = playTimeInMilliseconds / 60000; + + if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { + return t(isShort ? "amount_minutes_short" : "amount_minutes", { + amount: minutes.toFixed(0), + }); + } + + const hours = minutes / 60; + const hoursKey = isShort ? "amount_hours_short" : "amount_hours"; + const hoursAmount = isShort + ? Math.floor(hours) + : numberFormatter.format(hours); + + return t(hoursKey, { amount: hoursAmount }); + }, + [numberFormatter, t] + ); + + const handleCardClick = useCallback(() => { + navigate(buildGameDetailsPath(game)); + }, [navigate, game]); + + const handleContextMenuClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + onContextMenu(game, { x: e.clientX, y: e.clientY }); + }, + [game, onContextMenu] + ); + + const handleMenuButtonClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + const rect = e.currentTarget.getBoundingClientRect(); + onContextMenu(game, { x: rect.right, y: rect.bottom }); + }, + [game, onContextMenu] + ); + + return { + formatPlayTime, + handleCardClick, + handleContextMenuClick, + handleMenuButtonClick, + }; +} diff --git a/src/renderer/src/pages/library/library-game-card-large.tsx b/src/renderer/src/pages/library/library-game-card-large.tsx index 4cb54977..5c0e54c4 100644 --- a/src/renderer/src/pages/library/library-game-card-large.tsx +++ b/src/renderer/src/pages/library/library-game-card-large.tsx @@ -1,7 +1,5 @@ import { LibraryGame } from "@types"; -import { useDownload, useFormat } from "@renderer/hooks"; -import { useNavigate } from "react-router-dom"; -import { buildGameDetailsPath } from "@renderer/helpers"; +import { useDownload, useGameCard } from "@renderer/hooks"; import { PlayIcon, DownloadIcon, @@ -12,9 +10,8 @@ import { XIcon, } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; -import { useCallback, memo, useMemo } from "react"; +import { memo, useMemo } from "react"; import { useGameActions } from "@renderer/components/game-context-menu/use-game-actions"; -import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; import { logger } from "@renderer/logger"; import "./library-game-card-large.scss"; @@ -39,38 +36,17 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({ onContextMenu, }: Readonly) { const { t } = useTranslation("library"); - const { numberFormatter } = useFormat(); - const navigate = useNavigate(); const { lastPacket } = useDownload(); + const { + formatPlayTime, + handleCardClick, + handleContextMenuClick, + handleMenuButtonClick, + } = useGameCard(game, onContextMenu); const isGameDownloading = game?.download?.status === "active" && lastPacket?.gameId === game?.id; - const formatPlayTime = useCallback( - (playTimeInMilliseconds = 0, isShort = false) => { - const minutes = playTimeInMilliseconds / 60000; - - if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { - return t(isShort ? "amount_minutes_short" : "amount_minutes", { - amount: minutes.toFixed(0), - }); - } - - const hours = minutes / 60; - const hoursKey = isShort ? "amount_hours_short" : "amount_hours"; - const hoursAmount = isShort - ? Math.floor(hours) - : numberFormatter.format(hours); - - return t(hoursKey, { amount: hoursAmount }); - }, - [numberFormatter, t] - ); - - const handleCardClick = () => { - navigate(buildGameDetailsPath(game)); - }; - const { handlePlayGame, handleOpenDownloadOptions, @@ -101,24 +77,6 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({ } }; - const handleContextMenuClick = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onContextMenu(game, { x: e.clientX, y: e.clientY }); - }, - [game, onContextMenu] - ); - - const handleMenuButtonClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - const rect = e.currentTarget.getBoundingClientRect(); - onContextMenu(game, { x: rect.right, y: rect.bottom }); - }, - [game, onContextMenu] - ); - const backgroundImage = useMemo( () => getImageWithCustomPriority( diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx index 1b5b7afa..64053155 100644 --- a/src/renderer/src/pages/library/library-game-card.tsx +++ b/src/renderer/src/pages/library/library-game-card.tsx @@ -1,16 +1,12 @@ import { LibraryGame } from "@types"; -import { useFormat } from "@renderer/hooks"; -import { useNavigate } from "react-router-dom"; -import { useCallback, memo } from "react"; -import { buildGameDetailsPath } from "@renderer/helpers"; +import { useGameCard } from "@renderer/hooks"; +import { memo } from "react"; import { ClockIcon, AlertFillIcon, ThreeBarsIcon, TrophyIcon, } from "@primer/octicons-react"; -import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; -import { useTranslation } from "react-i18next"; import "./library-game-card.scss"; interface LibraryGameCardProps { @@ -31,52 +27,12 @@ export const LibraryGameCard = memo(function LibraryGameCard({ onMouseLeave, onContextMenu, }: Readonly) { - const { t } = useTranslation("library"); - const { numberFormatter } = useFormat(); - const navigate = useNavigate(); - - const formatPlayTime = useCallback( - (playTimeInMilliseconds = 0, isShort = false) => { - const minutes = playTimeInMilliseconds / 60000; - - if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { - return t(isShort ? "amount_minutes_short" : "amount_minutes", { - amount: minutes.toFixed(0), - }); - } - - const hours = minutes / 60; - const hoursKey = isShort ? "amount_hours_short" : "amount_hours"; - const hoursAmount = isShort - ? Math.floor(hours) - : numberFormatter.format(hours); - - return t(hoursKey, { amount: hoursAmount }); - }, - [numberFormatter, t] - ); - - const handleCardClick = () => { - navigate(buildGameDetailsPath(game)); - }; - - const handleContextMenuClick = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onContextMenu(game, { x: e.clientX, y: e.clientY }); - }, - [game, onContextMenu] - ); - - const handleMenuButtonClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - const rect = e.currentTarget.getBoundingClientRect(); - onContextMenu(game, { x: rect.right, y: rect.bottom }); - }, - [game, onContextMenu] - ); + const { + formatPlayTime, + handleCardClick, + handleContextMenuClick, + handleMenuButtonClick, + } = useGameCard(game, onContextMenu); const coverImage = game.coverImageUrl ?? From cf48627a8d1db8f71e9124748f271f8f2908f269 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 8 Nov 2025 14:51:10 +0200 Subject: [PATCH 4/7] fix: extracter ternary operation --- src/renderer/src/pages/library/library.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx index 4f1e7ed2..d9ef1c3c 100644 --- a/src/renderer/src/pages/library/library.tsx +++ b/src/renderer/src/pages/library/library.tsx @@ -158,8 +158,14 @@ export default function Library() { const hasGames = library.length > 0; - const itemHeight = - viewMode === "large" ? 200 : viewMode === "grid" ? 240 : 180; + let itemHeight: number; + if (viewMode === "large") { + itemHeight = 200; + } else if (viewMode === "grid") { + itemHeight = 240; + } else { + itemHeight = 180; + } return (
From 011559b499a16a31a1f102d3b4de7cd1e1119e41 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 8 Nov 2025 19:24:43 +0200 Subject: [PATCH 5/7] fix: removed VirtualList component from large view --- src/renderer/src/pages/library/library.tsx | 67 ++++------------------ 1 file changed, 11 insertions(+), 56 deletions(-) diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx index d9ef1c3c..323db73e 100644 --- a/src/renderer/src/pages/library/library.tsx +++ b/src/renderer/src/pages/library/library.tsx @@ -1,11 +1,10 @@ -import { useEffect, useMemo, useState, useCallback, useRef } from "react"; +import { useEffect, useMemo, useState, useCallback } from "react"; import { useLibrary, useAppDispatch, useAppSelector } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; import { TelescopeIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import { LibraryGame } from "@types"; import { GameContextMenu } from "@renderer/components"; -import VirtualList from "rc-virtual-list"; import { LibraryGameCard } from "./library-game-card"; import { LibraryGameCardLarge } from "./library-game-card-large"; import { ViewOptions, ViewMode } from "./view-options"; @@ -21,14 +20,12 @@ export default function Library() { const [viewMode, setViewMode] = useState("compact"); const [filterBy, setFilterBy] = useState("all"); - const [containerHeight, setContainerHeight] = useState(800); const [contextMenu, setContextMenu] = useState<{ game: LibraryGame | null; visible: boolean; position: { x: number; y: number }; }>({ game: null, visible: false, position: { x: 0, y: 0 } }); - - const containerRef = useRef(null); + const searchQuery = useAppSelector((state) => state.library.searchQuery); const dispatch = useAppDispatch(); const { t } = useTranslation("library"); @@ -57,19 +54,6 @@ export default function Library() { }; }, [dispatch, t, updateLibrary]); - useEffect(() => { - const updateHeight = () => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - setContainerHeight(window.innerHeight - rect.top); - } - }; - - updateHeight(); - window.addEventListener("resize", updateHeight); - return () => window.removeEventListener("resize", updateHeight); - }, []); - const handleOnMouseEnterGameCard = useCallback(() => { // Optional: pause animations if needed }, []); @@ -158,17 +142,8 @@ export default function Library() { const hasGames = library.length > 0; - let itemHeight: number; - if (viewMode === "large") { - itemHeight = 200; - } else if (viewMode === "grid") { - itemHeight = 240; - } else { - itemHeight = 180; - } - return ( -
+
{hasGames && (
@@ -200,38 +175,18 @@ export default function Library() {
)} - {hasGames && sortedLibrary.length > 50 && viewMode === "large" && ( + {hasGames && viewMode === "large" && (
- `${game.shop}-${game.objectId}`} - > - {(game) => ( - - )} - + {sortedLibrary.map((game) => ( + + ))}
)} - {hasGames && - (sortedLibrary.length <= 50 || viewMode !== "large") && - viewMode === "large" && ( -
- {sortedLibrary.map((game) => ( - - ))} -
- )} - {hasGames && viewMode !== "large" && (
    {sortedLibrary.map((game) => ( From 65e2bb38a095b284e09923a96a857d5e5ca3a02c Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 8 Nov 2025 19:26:13 +0200 Subject: [PATCH 6/7] ci: formatting --- src/renderer/src/pages/library/library.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx index 323db73e..7167809d 100644 --- a/src/renderer/src/pages/library/library.tsx +++ b/src/renderer/src/pages/library/library.tsx @@ -25,7 +25,7 @@ export default function Library() { visible: boolean; position: { x: number; y: number }; }>({ game: null, visible: false, position: { x: 0, y: 0 } }); - + const searchQuery = useAppSelector((state) => state.library.searchQuery); const dispatch = useAppDispatch(); const { t } = useTranslation("library"); From 46df34e8a584029366d264865548672907bc24f2 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 10 Nov 2025 22:20:44 +0000 Subject: [PATCH 7/7] feat: improving library --- src/locales/en/translation.json | 5 +- src/locales/es/translation.json | 22 ++++ src/locales/pt-BR/translation.json | 22 ++++ src/locales/ru/translation.json | 22 ++++ .../src/pages/library/filter-options.scss | 86 ++++++------ .../src/pages/library/filter-options.tsx | 122 ++++++++++++------ .../library/library-game-card-large.scss | 76 ----------- .../pages/library/library-game-card-large.tsx | 98 +------------- .../src/pages/library/library-game-card.scss | 49 +------ .../src/pages/library/library-game-card.tsx | 12 -- src/renderer/src/pages/library/library.scss | 5 + src/renderer/src/pages/library/library.tsx | 110 ++++++++-------- 12 files changed, 257 insertions(+), 372 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index cb2473eb..bcd774ea 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -719,9 +719,8 @@ "amount_minutes_short": "{{amount}}m", "manual_playtime_tooltip": "This playtime has been manually updated", "all_games": "All Games", - "favourited_games": "Favourited", - "new_games": "New Games", - "top_10": "Top 10" + "recently_played": "Recently Played", + "favorites": "Favorites" }, "achievement": { "achievement_unlocked": "Achievement unlocked", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index c7e9d13e..ad08777a 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -13,6 +13,7 @@ }, "sidebar": { "catalogue": "Catálogo", + "library": "Librería", "downloads": "Descargas", "settings": "Ajustes", "my_library": "Mi Librería", @@ -716,5 +717,26 @@ "hydra_cloud_feature_found": "¡Acabas de descubrir una característica de Hydra Cloud!", "learn_more": "Descubrir más", "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" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 50049140..002ec720 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -13,6 +13,7 @@ }, "sidebar": { "catalogue": "Catálogo", + "library": "Biblioteca", "downloads": "Downloads", "settings": "Ajustes", "my_library": "Biblioteca", @@ -731,5 +732,26 @@ "hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!", "learn_more": "Saiba mais", "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" } } diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 2e7c1504..02477701 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -13,6 +13,7 @@ }, "sidebar": { "catalogue": "Каталог", + "library": "Библиотека", "downloads": "Загрузки", "settings": "Настройки", "my_library": "Библиотека", @@ -727,5 +728,26 @@ "hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!", "learn_more": "Подробнее", "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": "Избранное" } } diff --git a/src/renderer/src/pages/library/filter-options.scss b/src/renderer/src/pages/library/filter-options.scss index 4831fd0e..25835072 100644 --- a/src/renderer/src/pages/library/filter-options.scss +++ b/src/renderer/src/pages/library/filter-options.scss @@ -1,63 +1,55 @@ @use "../../scss/globals.scss"; .library-filter-options { - &__container { + &__tabs { display: flex; - align-items: center; gap: calc(globals.$spacing-unit); - flex-wrap: wrap; + position: relative; } - &__option { - display: flex; - align-items: center; - gap: calc(globals.$spacing-unit); - padding: 8px 12px; - border-radius: 6px; - background: rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.9); + &__tab-wrapper { + position: relative; + } + + &__tab { + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + padding: calc(globals.$spacing-unit) calc(globals.$spacing-unit * 2); cursor: pointer; - font-size: 12px; + font-size: 14px; font-weight: 500; - transition: all ease 0.2s; - white-space: nowrap; /* prevent label and count from wrapping */ - border: 1px solid rgba(0, 0, 0, 0.06); + transition: color ease 0.2s; + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit * 0.5); - &:hover { - color: rgba(255, 255, 255, 0.9); - 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); - } + &--active { + color: white; } } - &__label { - font-weight: 500; - white-space: nowrap; - } - - &__count { - background: rgba(255, 255, 255, 0.16); - color: rgba(255, 255, 255, 0.95); - padding: 2px 8px; - border-radius: 4px; - font-size: 12px; + &__tab-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 6px; + background-color: rgba(255, 255, 255, 0.15); + border-radius: 9px; + font-size: 11px; font-weight: 600; - min-width: 24px; - text-align: center; - transition: all ease 0.2s; + color: rgba(255, 255, 255, 0.9); + line-height: 1; + } + + &__tab-underline { + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 2px; + background: white; } } diff --git a/src/renderer/src/pages/library/filter-options.tsx b/src/renderer/src/pages/library/filter-options.tsx index 572ebd35..cd22368f 100644 --- a/src/renderer/src/pages/library/filter-options.tsx +++ b/src/renderer/src/pages/library/filter-options.tsx @@ -1,61 +1,103 @@ +import { motion } from "framer-motion"; import { useTranslation } from "react-i18next"; import "./filter-options.scss"; -export type FilterOption = "all" | "favourited" | "new" | "top10"; +export type FilterOption = "all" | "recently_played" | "favorites"; interface FilterOptionsProps { filterBy: FilterOption; onFilterChange: (filterBy: FilterOption) => void; allGamesCount: number; - favouritedCount: number; - newGamesCount: number; - top10Count: number; + recentlyPlayedCount: number; + favoritesCount: number; } export function FilterOptions({ filterBy, onFilterChange, allGamesCount, - favouritedCount, - newGamesCount, - top10Count, + recentlyPlayedCount, + favoritesCount, }: Readonly) { const { t } = useTranslation("library"); return ( -
    - - - - +
    +
    + + {filterBy === "all" && ( + + )} +
    +
    + + {filterBy === "recently_played" && ( + + )} +
    +
    + + {filterBy === "favorites" && ( + + )} +
    ); } diff --git a/src/renderer/src/pages/library/library-game-card-large.scss b/src/renderer/src/pages/library/library-game-card-large.scss index 212fe80a..a06de7e9 100644 --- a/src/renderer/src/pages/library/library-game-card-large.scss +++ b/src/renderer/src/pages/library/library-game-card-large.scss @@ -84,36 +84,6 @@ 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 { flex: 1; @@ -238,50 +208,4 @@ 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; - } } diff --git a/src/renderer/src/pages/library/library-game-card-large.tsx b/src/renderer/src/pages/library/library-game-card-large.tsx index 5c0e54c4..c2bf6a3b 100644 --- a/src/renderer/src/pages/library/library-game-card-large.tsx +++ b/src/renderer/src/pages/library/library-game-card-large.tsx @@ -1,18 +1,11 @@ import { LibraryGame } from "@types"; -import { useDownload, useGameCard } from "@renderer/hooks"; +import { useGameCard } from "@renderer/hooks"; import { - PlayIcon, - DownloadIcon, ClockIcon, AlertFillIcon, - ThreeBarsIcon, TrophyIcon, - XIcon, } from "@primer/octicons-react"; -import { useTranslation } from "react-i18next"; 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"; interface LibraryGameCardLargeProps { @@ -35,48 +28,12 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({ game, onContextMenu, }: Readonly) { - const { t } = useTranslation("library"); - const { lastPacket } = useDownload(); const { formatPlayTime, handleCardClick, handleContextMenuClick, - handleMenuButtonClick, } = 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( () => getImageWithCustomPriority( @@ -129,14 +86,6 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({ {formatPlayTime(game.playTimeInMilliseconds)}
    -
@@ -183,51 +132,6 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
)} - -
diff --git a/src/renderer/src/pages/library/library-game-card.scss b/src/renderer/src/pages/library/library-game-card.scss index 1270e2aa..8643d23c 100644 --- a/src/renderer/src/pages/library/library-game-card.scss +++ b/src/renderer/src/pages/library/library-game-card.scss @@ -109,10 +109,10 @@ &__achievements { display: flex; flex-direction: column; - opacity: 0; - transform: translateY(8px); + opacity: 1; + transform: translateY(0); transition: all ease 0.2s; - pointer-events: none; + pointer-events: auto; width: 100%; } @@ -204,53 +204,12 @@ } } - &__menu-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 { + &__wrapper:hover &__action-button { opacity: 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 { object-fit: cover; diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx index 64053155..39cce681 100644 --- a/src/renderer/src/pages/library/library-game-card.tsx +++ b/src/renderer/src/pages/library/library-game-card.tsx @@ -4,7 +4,6 @@ import { memo } from "react"; import { ClockIcon, AlertFillIcon, - ThreeBarsIcon, TrophyIcon, } from "@primer/octicons-react"; import "./library-game-card.scss"; @@ -31,7 +30,6 @@ export const LibraryGameCard = memo(function LibraryGameCard({ formatPlayTime, handleCardClick, handleContextMenuClick, - handleMenuButtonClick, } = useGameCard(game, onContextMenu); const coverImage = @@ -69,18 +67,8 @@ export const LibraryGameCard = memo(function LibraryGameCard({ {formatPlayTime(game.playTimeInMilliseconds, true)} - - - {/* Achievements section - shown on hover */} {(game.achievementCount ?? 0) > 0 && (
diff --git a/src/renderer/src/pages/library/library.scss b/src/renderer/src/pages/library/library.scss index 40688084..ffc68b83 100644 --- a/src/renderer/src/pages/library/library.scss +++ b/src/renderer/src/pages/library/library.scss @@ -38,12 +38,17 @@ align-items: center; justify-content: space-between; width: 100%; + position: relative; } &__controls-left { display: flex; align-items: center; 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 { diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx index 7167809d..86afb549 100644 --- a/src/renderer/src/pages/library/library.tsx +++ b/src/renderer/src/pages/library/library.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState, useCallback } from "react"; +import { AnimatePresence, motion } from "framer-motion"; import { useLibrary, useAppDispatch, useAppSelector } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; import { TelescopeIcon } from "@primer/octicons-react"; @@ -77,23 +78,12 @@ export default function Library() { let filtered; switch (filterBy) { - case "favourited": + case "recently_played": + filtered = library.filter((game) => game.lastTimePlayed !== null); + break; + case "favorites": filtered = library.filter((game) => game.favorite); 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": default: filtered = library; @@ -124,19 +114,18 @@ export default function Library() { const filterCounts = useMemo(() => { const allGamesCount = library.length; - let favouritedCount = 0; - let newGamesCount = 0; + let recentlyPlayedCount = 0; + let favoritesCount = 0; for (const game of library) { - if (game.favorite) favouritedCount++; - if ((game.playTimeInMilliseconds || 0) === 0) newGamesCount++; + if (game.lastTimePlayed !== null) recentlyPlayedCount++; + if (game.favorite) favoritesCount++; } return { allGamesCount, - favouritedCount, - newGamesCount, - top10Count: Math.min(10, allGamesCount), + recentlyPlayedCount, + favoritesCount, }; }, [library]); @@ -152,9 +141,8 @@ export default function Library() { filterBy={filterBy} onFilterChange={setFilterBy} allGamesCount={filterCounts.allGamesCount} - favouritedCount={filterCounts.favouritedCount} - newGamesCount={filterCounts.newGamesCount} - top10Count={filterCounts.top10Count} + recentlyPlayedCount={filterCounts.recentlyPlayedCount} + favoritesCount={filterCounts.favoritesCount} />
@@ -175,34 +163,52 @@ export default function Library() {
)} - {hasGames && viewMode === "large" && ( -
- {sortedLibrary.map((game) => ( - - ))} -
- )} - - {hasGames && viewMode !== "large" && ( -
    - {sortedLibrary.map((game) => ( -
  • + {viewMode === "large" && ( + - -
  • - ))} -
+ {sortedLibrary.map((game) => ( + + ))} + + )} + + {viewMode !== "large" && ( + + {sortedLibrary.map((game) => ( +
  • + +
  • + ))} +
    + )} + )} {contextMenu.game && (