From dfd640ebdac8d2c203781f6b070c7fe857eabfee Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 00:14:47 +0300 Subject: [PATCH 01/11] feat: add revert title for non-custom games --- .../game-details/modals/edit-game-modal.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 04a27779..7bfc48fa 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -157,6 +157,24 @@ export function EditGameModal({ setAssetPath(assetType, ""); }; + const getOriginalTitle = (): string => { + if (!game) return ""; + + // For non-custom games, the original title is from shopDetails assets + return shopDetails?.assets?.title || game.title || ""; + }; + + const handleRestoreDefaultTitle = () => { + const originalTitle = getOriginalTitle(); + setGameName(originalTitle); + }; + + const isTitleChanged = (): boolean => { + if (!game || isCustomGame(game)) return false; + const originalTitle = getOriginalTitle(); + return gameName.trim() !== originalTitle.trim(); + }; + const [dragOverTarget, setDragOverTarget] = useState(null); const handleDragOver = (e: React.DragEvent) => { @@ -489,6 +507,19 @@ export function EditGameModal({ onChange={handleGameNameChange} theme="dark" disabled={isUpdating} + rightContent={ + isTitleChanged() && ( + + ) + } />
From f3b4898e9ce75ae653a21d67b3c453023c457beb Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 14:51:08 +0300 Subject: [PATCH 02/11] feat: proper cleanup of unused assets --- .../library/remove-game-from-library.ts | 53 +++++++++++++++++-- src/main/events/library/update-custom-game.ts | 38 +++++++++++++ .../library/update-game-custom-assets.ts | 29 ++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 6a33ffaf..4868d588 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,6 +1,6 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import { gamesSublevel, levelKeys } from "@main/level"; +import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop } from "@types"; const removeGameFromLibrary = async ( @@ -12,15 +12,62 @@ const removeGameFromLibrary = async ( const game = await gamesSublevel.get(gameKey); if (game) { - await gamesSublevel.put(gameKey, { + // Collect asset paths that need to be cleaned up before marking as deleted + const assetPathsToDelete: string[] = []; + + const assetUrls = game.shop === "custom" + ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] + : [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl]; + + assetUrls.forEach(url => { + if (url?.startsWith("local:")) { + assetPathsToDelete.push(url.replace("local:", "")); + } + }); + + + const updatedGame = { ...game, isDeleted: true, executablePath: null, - }); + ...(game.shop !== "custom" && { + customIconUrl: null, + customLogoImageUrl: null, + customHeroImageUrl: null, + }), + }; + + await gamesSublevel.put(gameKey, updatedGame); + + + if (game.shop !== "custom") { + const existingAssets = await gamesShopAssetsSublevel.get(gameKey); + if (existingAssets) { + const resetAssets = { + ...existingAssets, + title: existingAssets.title, + }; + await gamesShopAssetsSublevel.put(gameKey, resetAssets); + } + } if (game?.remoteId) { HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); } + + + if (assetPathsToDelete.length > 0) { + const fs = await import("fs"); + for (const assetPath of assetPathsToDelete) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); + } + } catch (error) { + console.warn(`Failed to delete asset ${assetPath}:`, error); + } + } + } } }; diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 6152c0df..168e0050 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -18,6 +18,30 @@ const updateCustomGame = async ( throw new Error("Game not found"); } + // Collect old asset paths that will be replaced + const oldAssetPaths: string[] = []; + + if (existingGame.iconUrl && iconUrl && existingGame.iconUrl !== iconUrl && existingGame.iconUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.iconUrl.replace("local:", "")); + } + if (existingGame.iconUrl && !iconUrl && existingGame.iconUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.iconUrl.replace("local:", "")); + } + + if (existingGame.logoImageUrl && logoImageUrl && existingGame.logoImageUrl !== logoImageUrl && existingGame.logoImageUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.logoImageUrl.replace("local:", "")); + } + if (existingGame.logoImageUrl && !logoImageUrl && existingGame.logoImageUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.logoImageUrl.replace("local:", "")); + } + + if (existingGame.libraryHeroImageUrl && libraryHeroImageUrl && existingGame.libraryHeroImageUrl !== libraryHeroImageUrl && existingGame.libraryHeroImageUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.libraryHeroImageUrl.replace("local:", "")); + } + if (existingGame.libraryHeroImageUrl && !libraryHeroImageUrl && existingGame.libraryHeroImageUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.libraryHeroImageUrl.replace("local:", "")); + } + const updatedGame = { ...existingGame, title, @@ -43,6 +67,20 @@ const updateCustomGame = async ( await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } + // Manually delete specific old asset files instead of running full cleanup + if (oldAssetPaths.length > 0) { + const fs = await import("fs"); + for (const assetPath of oldAssetPaths) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); + } + } catch (error) { + console.warn(`Failed to delete old asset ${assetPath}:`, error); + } + } + } + return updatedGame; }; diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 866cd60e..f8206904 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -18,6 +18,21 @@ const updateGameCustomAssets = async ( throw new Error("Game not found"); } + // Collect old custom asset paths that will be replaced + const oldAssetPaths: string[] = []; + + const assetPairs = [ + { existing: existingGame.customIconUrl, new: customIconUrl }, + { existing: existingGame.customLogoImageUrl, new: customLogoImageUrl }, + { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl } + ]; + + assetPairs.forEach(({ existing, new: newUrl }) => { + if (existing && newUrl !== undefined && existing !== newUrl && existing.startsWith("local:")) { + oldAssetPaths.push(existing.replace("local:", "")); + } + }); + const updatedGame = { ...existingGame, title, @@ -39,6 +54,20 @@ const updateGameCustomAssets = async ( await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } + // Manually delete specific old custom asset files instead of running full cleanup + if (oldAssetPaths.length > 0) { + const fs = await import("fs"); + for (const assetPath of oldAssetPaths) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); + } + } catch (error) { + console.warn(`Failed to delete old custom asset ${assetPath}:`, error); + } + } + } + return updatedGame; }; From 2bed7c0b37c83ff031ebe7b374e77e85458d9ed1 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 14:55:23 +0300 Subject: [PATCH 03/11] fix: cleaned comments and simplified function --- .../library/remove-game-from-library.ts | 22 +++++++------ src/main/events/library/update-custom-game.ts | 31 ++++++------------- .../library/update-game-custom-assets.ts | 18 ++++++----- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 4868d588..c4c8be9d 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -14,18 +14,22 @@ const removeGameFromLibrary = async ( if (game) { // Collect asset paths that need to be cleaned up before marking as deleted const assetPathsToDelete: string[] = []; - - const assetUrls = game.shop === "custom" - ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] - : [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl]; - - assetUrls.forEach(url => { + + const assetUrls = + game.shop === "custom" + ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] + : [ + game.customIconUrl, + game.customLogoImageUrl, + game.customHeroImageUrl, + ]; + + assetUrls.forEach((url) => { if (url?.startsWith("local:")) { assetPathsToDelete.push(url.replace("local:", "")); } }); - const updatedGame = { ...game, isDeleted: true, @@ -39,13 +43,12 @@ const removeGameFromLibrary = async ( await gamesSublevel.put(gameKey, updatedGame); - if (game.shop !== "custom") { const existingAssets = await gamesShopAssetsSublevel.get(gameKey); if (existingAssets) { const resetAssets = { ...existingAssets, - title: existingAssets.title, + title: existingAssets.title, }; await gamesShopAssetsSublevel.put(gameKey, resetAssets); } @@ -55,7 +58,6 @@ const removeGameFromLibrary = async ( HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); } - if (assetPathsToDelete.length > 0) { const fs = await import("fs"); for (const assetPath of assetPathsToDelete) { diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 168e0050..141e251e 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -18,29 +18,19 @@ const updateCustomGame = async ( throw new Error("Game not found"); } - // Collect old asset paths that will be replaced const oldAssetPaths: string[] = []; - if (existingGame.iconUrl && iconUrl && existingGame.iconUrl !== iconUrl && existingGame.iconUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.iconUrl.replace("local:", "")); - } - if (existingGame.iconUrl && !iconUrl && existingGame.iconUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.iconUrl.replace("local:", "")); - } + const assetPairs = [ + { existing: existingGame.iconUrl, new: iconUrl }, + { existing: existingGame.logoImageUrl, new: logoImageUrl }, + { existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl } + ]; - if (existingGame.logoImageUrl && logoImageUrl && existingGame.logoImageUrl !== logoImageUrl && existingGame.logoImageUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.logoImageUrl.replace("local:", "")); - } - if (existingGame.logoImageUrl && !logoImageUrl && existingGame.logoImageUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.logoImageUrl.replace("local:", "")); - } - - if (existingGame.libraryHeroImageUrl && libraryHeroImageUrl && existingGame.libraryHeroImageUrl !== libraryHeroImageUrl && existingGame.libraryHeroImageUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.libraryHeroImageUrl.replace("local:", "")); - } - if (existingGame.libraryHeroImageUrl && !libraryHeroImageUrl && existingGame.libraryHeroImageUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.libraryHeroImageUrl.replace("local:", "")); - } + assetPairs.forEach(({ existing, new: newUrl }) => { + if (existing?.startsWith("local:") && (!newUrl || existing !== newUrl)) { + oldAssetPaths.push(existing.replace("local:", "")); + } + }); const updatedGame = { ...existingGame, @@ -67,7 +57,6 @@ const updateCustomGame = async ( await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } - // Manually delete specific old asset files instead of running full cleanup if (oldAssetPaths.length > 0) { const fs = await import("fs"); for (const assetPath of oldAssetPaths) { diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index f8206904..392a0923 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -18,17 +18,21 @@ const updateGameCustomAssets = async ( throw new Error("Game not found"); } - // Collect old custom asset paths that will be replaced const oldAssetPaths: string[] = []; - + const assetPairs = [ { existing: existingGame.customIconUrl, new: customIconUrl }, { existing: existingGame.customLogoImageUrl, new: customLogoImageUrl }, - { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl } + { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl }, ]; - + assetPairs.forEach(({ existing, new: newUrl }) => { - if (existing && newUrl !== undefined && existing !== newUrl && existing.startsWith("local:")) { + if ( + existing && + newUrl !== undefined && + existing !== newUrl && + existing.startsWith("local:") + ) { oldAssetPaths.push(existing.replace("local:", "")); } }); @@ -43,18 +47,16 @@ const updateGameCustomAssets = async ( await gamesSublevel.put(gameKey, updatedGame); - // Also update the shop assets for non-custom games const existingAssets = await gamesShopAssetsSublevel.get(gameKey); if (existingAssets) { const updatedAssets = { ...existingAssets, - title, // Update the title in shop assets as well + title, }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } - // Manually delete specific old custom asset files instead of running full cleanup if (oldAssetPaths.length > 0) { const fs = await import("fs"); for (const assetPath of oldAssetPaths) { From 3e93a14deb066a3a674a3f2879fc39fbdd45acae Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 19:53:52 +0300 Subject: [PATCH 04/11] Fix: display actual image path in edit game modal --- .../library/update-game-custom-assets.ts | 2 +- .../game-details/modals/edit-game-modal.tsx | 60 ++++++++++++++++--- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 392a0923..1b75e0c4 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -51,7 +51,7 @@ const updateGameCustomAssets = async ( if (existingAssets) { const updatedAssets = { ...existingAssets, - title, + title, }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 7bfc48fa..0948a749 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { ImageIcon, XIcon } from "@primer/octicons-react"; @@ -32,6 +32,9 @@ export function EditGameModal({ const [iconPath, setIconPath] = useState(""); const [logoPath, setLogoPath] = useState(""); const [heroPath, setHeroPath] = useState(""); + const [iconDisplayPath, setIconDisplayPath] = useState(""); + const [logoDisplayPath, setLogoDisplayPath] = useState(""); + const [heroDisplayPath, setHeroDisplayPath] = useState(""); const [isUpdating, setIsUpdating] = useState(false); const [selectedAssetType, setSelectedAssetType] = useState("icon"); @@ -51,6 +54,10 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.iconUrl)); setLogoPath(extractLocalPath(game.logoImageUrl)); setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); + // For existing assets, show the asset path as display path since we don't have the original + setIconDisplayPath(extractLocalPath(game.iconUrl)); + setLogoDisplayPath(extractLocalPath(game.logoImageUrl)); + setHeroDisplayPath(extractLocalPath(game.libraryHeroImageUrl)); }, []); const setNonCustomGameAssets = useCallback( @@ -58,6 +65,10 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.customIconUrl)); setLogoPath(extractLocalPath(game.customLogoImageUrl)); setHeroPath(extractLocalPath(game.customHeroImageUrl)); + // For existing assets, show the asset path as display path since we don't have the original + setIconDisplayPath(extractLocalPath(game.customIconUrl)); + setLogoDisplayPath(extractLocalPath(game.customLogoImageUrl)); + setHeroDisplayPath(extractLocalPath(game.customHeroImageUrl)); setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); setDefaultLogoUrl( @@ -103,6 +114,17 @@ export function EditGameModal({ } }; + const getAssetDisplayPath = (assetType: AssetType): string => { + switch (assetType) { + case "icon": + return iconDisplayPath; + case "logo": + return logoDisplayPath; + case "hero": + return heroDisplayPath; + } + }; + const setAssetPath = (assetType: AssetType, path: string): void => { switch (assetType) { case "icon": @@ -117,6 +139,20 @@ export function EditGameModal({ } }; + const setAssetDisplayPath = (assetType: AssetType, path: string): void => { + switch (assetType) { + case "icon": + setIconDisplayPath(path); + break; + case "logo": + setLogoDisplayPath(path); + break; + case "hero": + setHeroDisplayPath(path); + break; + } + }; + const getDefaultUrl = (assetType: AssetType): string | null => { switch (assetType) { case "icon": @@ -140,21 +176,25 @@ export function EditGameModal({ }); if (filePaths && filePaths.length > 0) { + const originalPath = filePaths[0]; try { const copiedAssetUrl = await window.electron.copyCustomGameAsset( - filePaths[0], + originalPath, assetType ); setAssetPath(assetType, copiedAssetUrl.replace("local:", "")); + setAssetDisplayPath(assetType, originalPath); } catch (error) { console.error(`Failed to copy ${assetType} asset:`, error); - setAssetPath(assetType, filePaths[0]); + setAssetPath(assetType, originalPath); + setAssetDisplayPath(assetType, originalPath); } } }; const handleRestoreDefault = (assetType: AssetType) => { setAssetPath(assetType, ""); + setAssetDisplayPath(assetType, ""); }; const getOriginalTitle = (): string => { @@ -169,11 +209,11 @@ export function EditGameModal({ setGameName(originalTitle); }; - const isTitleChanged = (): boolean => { + const isTitleChanged = useMemo((): boolean => { if (!game || isCustomGame(game)) return false; const originalTitle = getOriginalTitle(); return gameName.trim() !== originalTitle.trim(); - }; + }, [game, gameName, shopDetails]); const [dragOverTarget, setDragOverTarget] = useState(null); @@ -250,6 +290,7 @@ export function EditGameModal({ const assetPath = copiedAssetUrl.replace("local:", ""); setAssetPath(assetType, assetPath); + setAssetDisplayPath(assetType, filePath); showSuccessToast( `${assetType.charAt(0).toUpperCase() + assetType.slice(1)} updated successfully!` @@ -361,7 +402,7 @@ export function EditGameModal({ }; // Helper function to reset form to initial state - const resetFormToInitialState = (game: LibraryGame | Game) => { + const resetFormToInitialState = useCallback((game: LibraryGame | Game) => { setGameName(game.title || ""); if (isCustomGame(game)) { @@ -373,7 +414,7 @@ export function EditGameModal({ } else { setNonCustomGameAssets(game as LibraryGame); } - }; + }, [setCustomGameAssets, setNonCustomGameAssets]); const handleClose = () => { if (!isUpdating && game) { @@ -396,6 +437,7 @@ export function EditGameModal({ const renderImageSection = (assetType: AssetType) => { const assetPath = getAssetPath(assetType); + const assetDisplayPath = getAssetDisplayPath(assetType); const defaultUrl = getDefaultUrl(assetType); const hasImage = assetPath || (game && !isCustomGame(game) && defaultUrl); const isDragOver = dragOverTarget === assetType; @@ -408,7 +450,7 @@ export function EditGameModal({
Date: Mon, 29 Sep 2025 20:09:04 +0300 Subject: [PATCH 05/11] fix: using for...of instead of forEach --- src/main/events/library/remove-game-from-library.ts | 4 ++-- src/main/events/library/update-custom-game.ts | 10 +++++----- src/main/events/library/update-game-custom-assets.ts | 6 +++--- .../src/pages/game-details/modals/edit-game-modal.tsx | 2 -- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index c4c8be9d..92539650 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -24,11 +24,11 @@ const removeGameFromLibrary = async ( game.customHeroImageUrl, ]; - assetUrls.forEach((url) => { + for (const url of assetUrls) { if (url?.startsWith("local:")) { assetPathsToDelete.push(url.replace("local:", "")); } - }); + } const updatedGame = { ...game, diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 141e251e..39f3551b 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -19,18 +19,18 @@ const updateCustomGame = async ( } const oldAssetPaths: string[] = []; - + const assetPairs = [ { existing: existingGame.iconUrl, new: iconUrl }, { existing: existingGame.logoImageUrl, new: logoImageUrl }, - { existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl } + { existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl }, ]; - - assetPairs.forEach(({ existing, new: newUrl }) => { + + for (const { existing, new: newUrl } of assetPairs) { if (existing?.startsWith("local:") && (!newUrl || existing !== newUrl)) { oldAssetPaths.push(existing.replace("local:", "")); } - }); + } const updatedGame = { ...existingGame, diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 1b75e0c4..166b2641 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -26,7 +26,7 @@ const updateGameCustomAssets = async ( { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl }, ]; - assetPairs.forEach(({ existing, new: newUrl }) => { + for (const { existing, new: newUrl } of assetPairs) { if ( existing && newUrl !== undefined && @@ -35,7 +35,7 @@ const updateGameCustomAssets = async ( ) { oldAssetPaths.push(existing.replace("local:", "")); } - }); + } const updatedGame = { ...existingGame, @@ -51,7 +51,7 @@ const updateGameCustomAssets = async ( if (existingAssets) { const updatedAssets = { ...existingAssets, - title, + title, }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 0948a749..5e8e2311 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -54,7 +54,6 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.iconUrl)); setLogoPath(extractLocalPath(game.logoImageUrl)); setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); - // For existing assets, show the asset path as display path since we don't have the original setIconDisplayPath(extractLocalPath(game.iconUrl)); setLogoDisplayPath(extractLocalPath(game.logoImageUrl)); setHeroDisplayPath(extractLocalPath(game.libraryHeroImageUrl)); @@ -65,7 +64,6 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.customIconUrl)); setLogoPath(extractLocalPath(game.customLogoImageUrl)); setHeroPath(extractLocalPath(game.customHeroImageUrl)); - // For existing assets, show the asset path as display path since we don't have the original setIconDisplayPath(extractLocalPath(game.customIconUrl)); setLogoDisplayPath(extractLocalPath(game.customLogoImageUrl)); setHeroDisplayPath(extractLocalPath(game.customHeroImageUrl)); From a87e04a366eb12985d1fa1282d634cd95cf74c05 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 20:14:36 +0300 Subject: [PATCH 06/11] Fix: using node:fs instead of fs --- .../library/remove-game-from-library.ts | 2 +- src/main/events/library/update-custom-game.ts | 2 +- .../library/update-game-custom-assets.ts | 4 +-- .../game-details/modals/edit-game-modal.tsx | 27 ++++++++++--------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 92539650..a9c0d272 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -59,7 +59,7 @@ const removeGameFromLibrary = async ( } if (assetPathsToDelete.length > 0) { - const fs = await import("fs"); + const fs = await import("node:fs"); for (const assetPath of assetPathsToDelete) { try { if (fs.existsSync(assetPath)) { diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 39f3551b..47641a6e 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -58,7 +58,7 @@ const updateCustomGame = async ( } if (oldAssetPaths.length > 0) { - const fs = await import("fs"); + const fs = await import("node:fs"); for (const assetPath of oldAssetPaths) { try { if (fs.existsSync(assetPath)) { diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 166b2641..4e86bbc0 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -51,14 +51,14 @@ const updateGameCustomAssets = async ( if (existingAssets) { const updatedAssets = { ...existingAssets, - title, + title, }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } if (oldAssetPaths.length > 0) { - const fs = await import("fs"); + const fs = await import("node:fs"); for (const assetPath of oldAssetPaths) { try { if (fs.existsSync(assetPath)) { diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 5e8e2311..3979d051 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -400,19 +400,22 @@ export function EditGameModal({ }; // Helper function to reset form to initial state - const resetFormToInitialState = useCallback((game: LibraryGame | Game) => { - setGameName(game.title || ""); + const resetFormToInitialState = useCallback( + (game: LibraryGame | Game) => { + setGameName(game.title || ""); - if (isCustomGame(game)) { - setCustomGameAssets(game); - // Clear default URLs for custom games - setDefaultIconUrl(null); - setDefaultLogoUrl(null); - setDefaultHeroUrl(null); - } else { - setNonCustomGameAssets(game as LibraryGame); - } - }, [setCustomGameAssets, setNonCustomGameAssets]); + if (isCustomGame(game)) { + setCustomGameAssets(game); + // Clear default URLs for custom games + setDefaultIconUrl(null); + setDefaultLogoUrl(null); + setDefaultHeroUrl(null); + } else { + setNonCustomGameAssets(game as LibraryGame); + } + }, + [setCustomGameAssets, setNonCustomGameAssets] + ); const handleClose = () => { if (!isUpdating && game) { From 96d6b90356c7612210c5358937d872f6d320bdca Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 20:20:58 +0300 Subject: [PATCH 07/11] Fix: Refactoring functions to reduce complexity --- .../library/remove-game-from-library.ts | 133 ++++++++++-------- .../library/update-game-custom-assets.ts | 90 ++++++++---- 2 files changed, 141 insertions(+), 82 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index a9c0d272..438aa39a 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,7 +1,69 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; -import type { GameShop } from "@types"; +import type { GameShop, Game } from "@types"; + +const collectAssetPathsToDelete = (game: Game): string[] => { + const assetPathsToDelete: string[] = []; + + const assetUrls = + game.shop === "custom" + ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] + : [ + game.customIconUrl, + game.customLogoImageUrl, + game.customHeroImageUrl, + ]; + + for (const url of assetUrls) { + if (url?.startsWith("local:")) { + assetPathsToDelete.push(url.replace("local:", "")); + } + } + + return assetPathsToDelete; +}; + +const updateGameAsDeleted = async (game: Game, gameKey: string): Promise => { + const updatedGame = { + ...game, + isDeleted: true, + executablePath: null, + ...(game.shop !== "custom" && { + customIconUrl: null, + customLogoImageUrl: null, + customHeroImageUrl: null, + }), + }; + + await gamesSublevel.put(gameKey, updatedGame); +}; + +const resetShopAssets = async (gameKey: string): Promise => { + const existingAssets = await gamesShopAssetsSublevel.get(gameKey); + if (existingAssets) { + const resetAssets = { + ...existingAssets, + title: existingAssets.title, + }; + await gamesShopAssetsSublevel.put(gameKey, resetAssets); + } +}; + +const deleteAssetFiles = async (assetPathsToDelete: string[]): Promise => { + if (assetPathsToDelete.length === 0) return; + + const fs = await import("node:fs"); + for (const assetPath of assetPathsToDelete) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); + } + } catch (error) { + console.warn(`Failed to delete asset ${assetPath}:`, error); + } + } +}; const removeGameFromLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -11,66 +73,21 @@ const removeGameFromLibrary = async ( const gameKey = levelKeys.game(shop, objectId); const game = await gamesSublevel.get(gameKey); - if (game) { - // Collect asset paths that need to be cleaned up before marking as deleted - const assetPathsToDelete: string[] = []; + if (!game) return; - const assetUrls = - game.shop === "custom" - ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] - : [ - game.customIconUrl, - game.customLogoImageUrl, - game.customHeroImageUrl, - ]; + const assetPathsToDelete = collectAssetPathsToDelete(game); + + await updateGameAsDeleted(game, gameKey); - for (const url of assetUrls) { - if (url?.startsWith("local:")) { - assetPathsToDelete.push(url.replace("local:", "")); - } - } - - const updatedGame = { - ...game, - isDeleted: true, - executablePath: null, - ...(game.shop !== "custom" && { - customIconUrl: null, - customLogoImageUrl: null, - customHeroImageUrl: null, - }), - }; - - await gamesSublevel.put(gameKey, updatedGame); - - if (game.shop !== "custom") { - const existingAssets = await gamesShopAssetsSublevel.get(gameKey); - if (existingAssets) { - const resetAssets = { - ...existingAssets, - title: existingAssets.title, - }; - await gamesShopAssetsSublevel.put(gameKey, resetAssets); - } - } - - if (game?.remoteId) { - HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); - } - - if (assetPathsToDelete.length > 0) { - const fs = await import("node:fs"); - for (const assetPath of assetPathsToDelete) { - try { - if (fs.existsSync(assetPath)) { - await fs.promises.unlink(assetPath); - } - } catch (error) { - console.warn(`Failed to delete asset ${assetPath}:`, error); - } - } - } + if (game.shop !== "custom") { + await resetShopAssets(gameKey); } + + if (game?.remoteId) { + HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); + } + + await deleteAssetFiles(assetPathsToDelete); }; registerEvent("removeGameFromLibrary", removeGameFromLibrary); diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 4e86bbc0..2138af3a 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -1,23 +1,13 @@ import { registerEvent } from "../register-event"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; -import type { GameShop } from "@types"; +import type { GameShop, Game } from "@types"; -const updateGameCustomAssets = async ( - _event: Electron.IpcMainInvokeEvent, - shop: GameShop, - objectId: string, - title: string, +const collectOldAssetPaths = ( + existingGame: Game, customIconUrl?: string | null, customLogoImageUrl?: string | null, customHeroImageUrl?: string | null -) => { - const gameKey = levelKeys.game(shop, objectId); - - const existingGame = await gamesSublevel.get(gameKey); - if (!existingGame) { - throw new Error("Game not found"); - } - +): string[] => { const oldAssetPaths: string[] = []; const assetPairs = [ @@ -37,6 +27,17 @@ const updateGameCustomAssets = async ( } } + return oldAssetPaths; +}; + +const updateGameData = async ( + gameKey: string, + existingGame: Game, + title: string, + customIconUrl?: string | null, + customLogoImageUrl?: string | null, + customHeroImageUrl?: string | null +): Promise => { const updatedGame = { ...existingGame, title, @@ -46,29 +47,70 @@ const updateGameCustomAssets = async ( }; await gamesSublevel.put(gameKey, updatedGame); + return updatedGame; +}; +const updateShopAssets = async (gameKey: string, title: string): Promise => { const existingAssets = await gamesShopAssetsSublevel.get(gameKey); if (existingAssets) { const updatedAssets = { ...existingAssets, title, }; - await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } +}; - if (oldAssetPaths.length > 0) { - const fs = await import("node:fs"); - for (const assetPath of oldAssetPaths) { - try { - if (fs.existsSync(assetPath)) { - await fs.promises.unlink(assetPath); - } - } catch (error) { - console.warn(`Failed to delete old custom asset ${assetPath}:`, error); +const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise => { + if (oldAssetPaths.length === 0) return; + + const fs = await import("node:fs"); + for (const assetPath of oldAssetPaths) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); } + } catch (error) { + console.warn(`Failed to delete old custom asset ${assetPath}:`, error); } } +}; + +const updateGameCustomAssets = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + title: string, + customIconUrl?: string | null, + customLogoImageUrl?: string | null, + customHeroImageUrl?: string | null +) => { + const gameKey = levelKeys.game(shop, objectId); + + const existingGame = await gamesSublevel.get(gameKey); + if (!existingGame) { + throw new Error("Game not found"); + } + + const oldAssetPaths = collectOldAssetPaths( + existingGame, + customIconUrl, + customLogoImageUrl, + customHeroImageUrl + ); + + const updatedGame = await updateGameData( + gameKey, + existingGame, + title, + customIconUrl, + customLogoImageUrl, + customHeroImageUrl + ); + + await updateShopAssets(gameKey, title); + + await deleteOldAssetFiles(oldAssetPaths); return updatedGame; }; From 7e22344f77a0cab9bb597d89b50c17ff5b50c4f0 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 20:36:05 +0300 Subject: [PATCH 08/11] Fix: fixed fs import and started using logger.warn --- .../library/remove-game-from-library.ts | 28 ++++++++++--------- src/main/events/library/update-custom-game.ts | 5 ++-- .../library/update-game-custom-assets.ts | 5 ++-- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 438aa39a..b7033147 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -2,29 +2,30 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop, Game } from "@types"; +import fs from "node:fs"; +import { logger } from "@main/services"; const collectAssetPathsToDelete = (game: Game): string[] => { const assetPathsToDelete: string[] = []; - + const assetUrls = game.shop === "custom" ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] - : [ - game.customIconUrl, - game.customLogoImageUrl, - game.customHeroImageUrl, - ]; + : [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl]; for (const url of assetUrls) { if (url?.startsWith("local:")) { assetPathsToDelete.push(url.replace("local:", "")); } } - + return assetPathsToDelete; }; -const updateGameAsDeleted = async (game: Game, gameKey: string): Promise => { +const updateGameAsDeleted = async ( + game: Game, + gameKey: string +): Promise => { const updatedGame = { ...game, isDeleted: true, @@ -50,17 +51,18 @@ const resetShopAssets = async (gameKey: string): Promise => { } }; -const deleteAssetFiles = async (assetPathsToDelete: string[]): Promise => { +const deleteAssetFiles = async ( + assetPathsToDelete: string[] +): Promise => { if (assetPathsToDelete.length === 0) return; - - const fs = await import("node:fs"); + for (const assetPath of assetPathsToDelete) { try { if (fs.existsSync(assetPath)) { await fs.promises.unlink(assetPath); } } catch (error) { - console.warn(`Failed to delete asset ${assetPath}:`, error); + logger.warn(`Failed to delete asset ${assetPath}:`, error); } } }; @@ -76,7 +78,7 @@ const removeGameFromLibrary = async ( if (!game) return; const assetPathsToDelete = collectAssetPathsToDelete(game); - + await updateGameAsDeleted(game, gameKey); if (game.shop !== "custom") { diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 47641a6e..82d7f45f 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -1,6 +1,8 @@ import { registerEvent } from "../register-event"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop } from "@types"; +import fs from "node:fs"; +import { logger } from "@main/services"; const updateCustomGame = async ( _event: Electron.IpcMainInvokeEvent, @@ -58,14 +60,13 @@ const updateCustomGame = async ( } if (oldAssetPaths.length > 0) { - const fs = await import("node:fs"); for (const assetPath of oldAssetPaths) { try { if (fs.existsSync(assetPath)) { await fs.promises.unlink(assetPath); } } catch (error) { - console.warn(`Failed to delete old asset ${assetPath}:`, error); + logger.warn(`Failed to delete old asset ${assetPath}:`, error); } } } diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 2138af3a..1813e8a8 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -1,6 +1,8 @@ import { registerEvent } from "../register-event"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop, Game } from "@types"; +import fs from "node:fs"; +import { logger } from "@main/services"; const collectOldAssetPaths = ( existingGame: Game, @@ -64,14 +66,13 @@ const updateShopAssets = async (gameKey: string, title: string): Promise = const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise => { if (oldAssetPaths.length === 0) return; - const fs = await import("node:fs"); for (const assetPath of oldAssetPaths) { try { if (fs.existsSync(assetPath)) { await fs.promises.unlink(assetPath); } } catch (error) { - console.warn(`Failed to delete old custom asset ${assetPath}:`, error); + logger.warn(`Failed to delete old custom asset ${assetPath}:`, error); } } }; From a39f9ebb70abab4e598d820b9f793c64ef73d2e5 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 20:39:58 +0300 Subject: [PATCH 09/11] fix: multiple imports --- src/main/events/library/remove-game-from-library.ts | 3 +-- src/main/events/library/update-game-custom-assets.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index b7033147..fbb60ab2 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,9 +1,8 @@ import { registerEvent } from "../register-event"; -import { HydraApi } from "@main/services"; +import { HydraApi, logger } from "@main/services"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop, Game } from "@types"; import fs from "node:fs"; -import { logger } from "@main/services"; const collectAssetPathsToDelete = (game: Game): string[] => { const assetPathsToDelete: string[] = []; diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 1813e8a8..4bd4e517 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -52,7 +52,10 @@ const updateGameData = async ( return updatedGame; }; -const updateShopAssets = async (gameKey: string, title: string): Promise => { +const updateShopAssets = async ( + gameKey: string, + title: string +): Promise => { const existingAssets = await gamesShopAssetsSublevel.get(gameKey); if (existingAssets) { const updatedAssets = { From bd053a1635fae69049688b80d6d04c702cfe6e57 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 22:04:01 +0300 Subject: [PATCH 10/11] fix: favorite and pin button overlapping playtime --- src/locales/en/translation.json | 2 + src/locales/ru/translation.json | 2 + .../user-library-game-card.scss | 2 +- .../user-library-game-card.tsx | 56 ++++++++++++++++++- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ce8b4de1..93fd5b0a 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -506,6 +506,8 @@ "user_profile": { "amount_hours": "{{amount}} hours", "amount_minutes": "{{amount}} minutes", + "amount_hours_short": "{{amount}}h", + "amount_minutes_short": "{{amount}}m", "last_time_played": "Last played {{period}}", "activity": "Recent Activity", "library": "Library", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 58235989..c92d7902 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -486,6 +486,8 @@ "user_profile": { "amount_hours": "{{amount}} часов", "amount_minutes": "{{amount}} минут", + "amount_hours_short": "{{amount}}ч", + "amount_minutes_short": "{{amount}}м", "last_time_played": "Последняя игра {{period}}", "activity": "Недавняя активность", "library": "Библиотека", diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index f072fdd5..e40061de 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -86,7 +86,7 @@ top: 8px; right: 8px; display: flex; - gap: 6px; + gap: 4px; z-index: 2; } diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index 860c6758..d30dbf8a 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -2,7 +2,7 @@ import { UserGame } from "@types"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import { useFormat, useToast } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { useCallback, useContext, useState } from "react"; +import { useCallback, useContext, useState, useEffect, useRef } from "react"; import { buildGameAchievementPath, buildGameDetailsPath, @@ -43,6 +43,9 @@ export function UserLibraryGameCard({ const navigate = useNavigate(); const [isTooltipHovered, setIsTooltipHovered] = useState(false); const [isPinning, setIsPinning] = useState(false); + const [useShortFormat, setUseShortFormat] = useState(false); + const cardRef = useRef(null); + const playtimeRef = useRef(null); const getStatsItemCount = useCallback(() => { let statsCount = 1; @@ -94,6 +97,50 @@ export function UserLibraryGameCard({ [numberFormatter, t] ); + const formatPlayTimeShort = useCallback( + (playTimeInSeconds = 0) => { + const minutes = playTimeInSeconds / 60; + + if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { + return t("amount_minutes_short", { + amount: minutes.toFixed(0), + }); + } + + const hours = minutes / 60; + return t("amount_hours_short", { amount: Math.floor(hours) }); + }, + [t] + ); + + const checkForOverlap = useCallback(() => { + if (!cardRef.current || !playtimeRef.current) return; + + const cardWidth = cardRef.current.offsetWidth; + const hasButtons = game.isFavorite || isMe; + + if (hasButtons && cardWidth < 180) { + setUseShortFormat(true); + } else { + setUseShortFormat(false); + } + }, [game.isFavorite, isMe]); + + useEffect(() => { + checkForOverlap(); + + const handleResize = () => { + checkForOverlap(); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [checkForOverlap]); + + useEffect(() => { + checkForOverlap(); + }, [game.isFavorite, isMe, checkForOverlap]); + const toggleGamePinned = async () => { setIsPinning(true); @@ -119,6 +166,7 @@ export function UserLibraryGameCard({ return ( <>
  • )} )} - {formatPlayTime(game.playTimeInSeconds)} + {useShortFormat + ? formatPlayTimeShort(game.playTimeInSeconds) + : formatPlayTime(game.playTimeInSeconds) + } {userProfile?.hasActiveSubscription && From 9689c19863c1ddfc1fce0f05767c98c68d3e5d61 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 30 Sep 2025 00:52:46 +0300 Subject: [PATCH 11/11] fix: state fix and remake checking for overlap --- .../game-details/modals/edit-game-modal.tsx | 144 +++++++----------- .../user-library-game-card.scss | 20 +++ .../user-library-game-card.tsx | 73 ++------- 3 files changed, 93 insertions(+), 144 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 3979d051..2413cb9e 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -29,19 +29,24 @@ export function EditGameModal({ const { showSuccessToast, showErrorToast } = useToast(); const [gameName, setGameName] = useState(""); - const [iconPath, setIconPath] = useState(""); - const [logoPath, setLogoPath] = useState(""); - const [heroPath, setHeroPath] = useState(""); - const [iconDisplayPath, setIconDisplayPath] = useState(""); - const [logoDisplayPath, setLogoDisplayPath] = useState(""); - const [heroDisplayPath, setHeroDisplayPath] = useState(""); + const [assetPaths, setAssetPaths] = useState({ + icon: "", + logo: "", + hero: "", + }); + const [assetDisplayPaths, setAssetDisplayPaths] = useState({ + icon: "", + logo: "", + hero: "", + }); + const [defaultUrls, setDefaultUrls] = useState({ + icon: null as string | null, + logo: null as string | null, + hero: null as string | null, + }); const [isUpdating, setIsUpdating] = useState(false); const [selectedAssetType, setSelectedAssetType] = useState("icon"); - const [defaultIconUrl, setDefaultIconUrl] = useState(null); - const [defaultLogoUrl, setDefaultLogoUrl] = useState(null); - const [defaultHeroUrl, setDefaultHeroUrl] = useState(null); - const isCustomGame = (game: LibraryGame | Game): boolean => { return game.shop === "custom"; }; @@ -51,32 +56,36 @@ export function EditGameModal({ }; const setCustomGameAssets = useCallback((game: LibraryGame | Game) => { - setIconPath(extractLocalPath(game.iconUrl)); - setLogoPath(extractLocalPath(game.logoImageUrl)); - setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); - setIconDisplayPath(extractLocalPath(game.iconUrl)); - setLogoDisplayPath(extractLocalPath(game.logoImageUrl)); - setHeroDisplayPath(extractLocalPath(game.libraryHeroImageUrl)); + setAssetPaths({ + icon: extractLocalPath(game.iconUrl), + logo: extractLocalPath(game.logoImageUrl), + hero: extractLocalPath(game.libraryHeroImageUrl), + }); + setAssetDisplayPaths({ + icon: extractLocalPath(game.iconUrl), + logo: extractLocalPath(game.logoImageUrl), + hero: extractLocalPath(game.libraryHeroImageUrl), + }); }, []); const setNonCustomGameAssets = useCallback( (game: LibraryGame) => { - setIconPath(extractLocalPath(game.customIconUrl)); - setLogoPath(extractLocalPath(game.customLogoImageUrl)); - setHeroPath(extractLocalPath(game.customHeroImageUrl)); - setIconDisplayPath(extractLocalPath(game.customIconUrl)); - setLogoDisplayPath(extractLocalPath(game.customLogoImageUrl)); - setHeroDisplayPath(extractLocalPath(game.customHeroImageUrl)); + setAssetPaths({ + icon: extractLocalPath(game.customIconUrl), + logo: extractLocalPath(game.customLogoImageUrl), + hero: extractLocalPath(game.customHeroImageUrl), + }); + setAssetDisplayPaths({ + icon: extractLocalPath(game.customIconUrl), + logo: extractLocalPath(game.customLogoImageUrl), + hero: extractLocalPath(game.customHeroImageUrl), + }); - setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); - setDefaultLogoUrl( - shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null - ); - setDefaultHeroUrl( - shopDetails?.assets?.libraryHeroImageUrl || - game.libraryHeroImageUrl || - null - ); + setDefaultUrls({ + icon: shopDetails?.assets?.iconUrl || game.iconUrl || null, + logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null, + hero: shopDetails?.assets?.libraryHeroImageUrl || game.libraryHeroImageUrl || null, + }); }, [shopDetails] ); @@ -102,64 +111,23 @@ export function EditGameModal({ }; const getAssetPath = (assetType: AssetType): string => { - switch (assetType) { - case "icon": - return iconPath; - case "logo": - return logoPath; - case "hero": - return heroPath; - } + return assetPaths[assetType]; }; const getAssetDisplayPath = (assetType: AssetType): string => { - switch (assetType) { - case "icon": - return iconDisplayPath; - case "logo": - return logoDisplayPath; - case "hero": - return heroDisplayPath; - } + return assetDisplayPaths[assetType]; }; const setAssetPath = (assetType: AssetType, path: string): void => { - switch (assetType) { - case "icon": - setIconPath(path); - break; - case "logo": - setLogoPath(path); - break; - case "hero": - setHeroPath(path); - break; - } + setAssetPaths(prev => ({ ...prev, [assetType]: path })); }; const setAssetDisplayPath = (assetType: AssetType, path: string): void => { - switch (assetType) { - case "icon": - setIconDisplayPath(path); - break; - case "logo": - setLogoDisplayPath(path); - break; - case "hero": - setHeroDisplayPath(path); - break; - } + setAssetDisplayPaths(prev => ({ ...prev, [assetType]: path })); }; const getDefaultUrl = (assetType: AssetType): string | null => { - switch (assetType) { - case "icon": - return defaultIconUrl; - case "logo": - return defaultLogoUrl; - case "hero": - return defaultHeroUrl; - } + return defaultUrls[assetType]; }; const handleSelectAsset = async (assetType: AssetType) => { @@ -324,10 +292,10 @@ export function EditGameModal({ // Helper function to prepare custom game assets const prepareCustomGameAssets = (game: LibraryGame | Game) => { - const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl; - const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl; - const libraryHeroImageUrl = heroPath - ? `local:${heroPath}` + const iconUrl = assetPaths.icon ? `local:${assetPaths.icon}` : game.iconUrl; + const logoImageUrl = assetPaths.logo ? `local:${assetPaths.logo}` : game.logoImageUrl; + const libraryHeroImageUrl = assetPaths.hero + ? `local:${assetPaths.hero}` : game.libraryHeroImageUrl; return { iconUrl, logoImageUrl, libraryHeroImageUrl }; @@ -336,9 +304,9 @@ export function EditGameModal({ // Helper function to prepare non-custom game assets const prepareNonCustomGameAssets = () => { return { - customIconUrl: iconPath ? `local:${iconPath}` : null, - customLogoImageUrl: logoPath ? `local:${logoPath}` : null, - customHeroImageUrl: heroPath ? `local:${heroPath}` : null, + customIconUrl: assetPaths.icon ? `local:${assetPaths.icon}` : null, + customLogoImageUrl: assetPaths.logo ? `local:${assetPaths.logo}` : null, + customHeroImageUrl: assetPaths.hero ? `local:${assetPaths.hero}` : null, }; }; @@ -407,9 +375,11 @@ export function EditGameModal({ if (isCustomGame(game)) { setCustomGameAssets(game); // Clear default URLs for custom games - setDefaultIconUrl(null); - setDefaultLogoUrl(null); - setDefaultHeroUrl(null); + setDefaultUrls({ + icon: null, + logo: null, + hero: null, + }); } else { setNonCustomGameAssets(game as LibraryGame); } diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index e40061de..dccd9dd1 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -8,6 +8,7 @@ display: flex; transition: all ease 0.2s; cursor: grab; + container-type: inline-size; &:hover { transform: scale(1.05); @@ -160,6 +161,25 @@ transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } + + &-long { + display: inline; + } + + &-short { + display: none; + } + + // When the card is narrow (less than 180px), show short format + @container (max-width: 180px) { + &-long { + display: none; + } + + &-short { + display: inline; + } + } } &__manual-playtime { color: globals.$warning-color; diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index d30dbf8a..ec7736e0 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -2,7 +2,7 @@ import { UserGame } from "@types"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import { useFormat, useToast } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { useCallback, useContext, useState, useEffect, useRef } from "react"; +import { useCallback, useContext, useState } from "react"; import { buildGameAchievementPath, buildGameDetailsPath, @@ -43,9 +43,7 @@ export function UserLibraryGameCard({ const navigate = useNavigate(); const [isTooltipHovered, setIsTooltipHovered] = useState(false); const [isPinning, setIsPinning] = useState(false); - const [useShortFormat, setUseShortFormat] = useState(false); - const cardRef = useRef(null); - const playtimeRef = useRef(null); + const getStatsItemCount = useCallback(() => { let statsCount = 1; @@ -82,64 +80,25 @@ export function UserLibraryGameCard({ }; const formatPlayTime = useCallback( - (playTimeInSeconds = 0) => { + (playTimeInSeconds = 0, isShort = false) => { const minutes = playTimeInSeconds / 60; if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { - return t("amount_minutes", { + return t(isShort ? "amount_minutes_short" : "amount_minutes", { amount: minutes.toFixed(0), }); } const hours = minutes / 60; - return t("amount_hours", { amount: numberFormatter.format(hours) }); + 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 formatPlayTimeShort = useCallback( - (playTimeInSeconds = 0) => { - const minutes = playTimeInSeconds / 60; - if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { - return t("amount_minutes_short", { - amount: minutes.toFixed(0), - }); - } - - const hours = minutes / 60; - return t("amount_hours_short", { amount: Math.floor(hours) }); - }, - [t] - ); - - const checkForOverlap = useCallback(() => { - if (!cardRef.current || !playtimeRef.current) return; - - const cardWidth = cardRef.current.offsetWidth; - const hasButtons = game.isFavorite || isMe; - - if (hasButtons && cardWidth < 180) { - setUseShortFormat(true); - } else { - setUseShortFormat(false); - } - }, [game.isFavorite, isMe]); - - useEffect(() => { - checkForOverlap(); - - const handleResize = () => { - checkForOverlap(); - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [checkForOverlap]); - - useEffect(() => { - checkForOverlap(); - }, [game.isFavorite, isMe, checkForOverlap]); const toggleGamePinned = async () => { setIsPinning(true); @@ -166,7 +125,6 @@ export function UserLibraryGameCard({ return ( <>
  • )} - )} - {useShortFormat - ? formatPlayTimeShort(game.playTimeInSeconds) - : formatPlayTime(game.playTimeInSeconds) - } - + + {formatPlayTime(game.playTimeInSeconds)} + + + {formatPlayTime(game.playTimeInSeconds, true)} + +
  • {userProfile?.hasActiveSubscription && game.achievementCount > 0 && (