From c3a4990a507cca27396e5dbe537e898e66df22dc Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 8 Nov 2025 14:28:54 +0200 Subject: [PATCH] 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 && ( + + )}
); }