diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index a3df02118..389116e9c 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -81,6 +81,9 @@ "edit_game_modal_hero": "Library Hero", "edit_game_modal_select_hero": "Select library hero image", "edit_game_modal_hero_preview": "Library hero image preview", + "edit_game_modal_cover": "Cover", + "edit_game_modal_select_cover": "Select library cover image", + "edit_game_modal_cover_preview": "Cover image preview", "edit_game_modal_cancel": "Cancel", "edit_game_modal_update": "Update", "edit_game_modal_updating": "Updating...", @@ -91,13 +94,16 @@ "edit_game_modal_icon_resolution": "Recommended resolution: 256x256px", "edit_game_modal_logo_resolution": "Recommended resolution: 640x360px", "edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px", + "edit_game_modal_cover_resolution": "Recommended resolution: 600x900px", "edit_game_modal_assets": "Assets", "edit_game_modal_drop_icon_image_here": "Drop icon image here", "edit_game_modal_drop_logo_image_here": "Drop logo image here", "edit_game_modal_drop_hero_image_here": "Drop hero image here", + "edit_game_modal_drop_cover_image_here": "Drop cover image here", "edit_game_modal_drop_to_replace_icon": "Drop to replace icon", "edit_game_modal_drop_to_replace_logo": "Drop to replace logo", "edit_game_modal_drop_to_replace_hero": "Drop to replace hero", + "edit_game_modal_drop_to_replace_cover": "Drop to replace cover", "install_decky_plugin": "Install Decky Plugin", "update_decky_plugin": "Update Decky Plugin", "decky_plugin_installed_version": "Decky Plugin (v{{version}})", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 8cafa628b..b67e1599b 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -81,6 +81,9 @@ "edit_game_modal_hero": "Library Hero", "edit_game_modal_select_hero": "Select library hero image", "edit_game_modal_hero_preview": "Library hero image preview", + "edit_game_modal_cover": "Cover", + "edit_game_modal_select_cover": "Select library cover image", + "edit_game_modal_cover_preview": "Cover image preview", "edit_game_modal_cancel": "Cancel", "edit_game_modal_update": "Update", "edit_game_modal_updating": "Updating...", @@ -91,13 +94,16 @@ "edit_game_modal_icon_resolution": "Recommended resolution: 256x256px", "edit_game_modal_logo_resolution": "Recommended resolution: 640x360px", "edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px", + "edit_game_modal_cover_resolution": "Recommended resolution: 600x900px", "edit_game_modal_assets": "Assets", "edit_game_modal_drop_icon_image_here": "Drop icon image here", "edit_game_modal_drop_logo_image_here": "Drop logo image here", "edit_game_modal_drop_hero_image_here": "Drop hero image here", + "edit_game_modal_drop_cover_image_here": "Drop cover image here", "edit_game_modal_drop_to_replace_icon": "Drop to replace icon", "edit_game_modal_drop_to_replace_logo": "Drop to replace logo", "edit_game_modal_drop_to_replace_hero": "Drop to replace hero", + "edit_game_modal_drop_to_replace_cover": "Drop to replace cover", "install_decky_plugin": "Install Decky Plugin", "update_decky_plugin": "Update Decky Plugin", "decky_plugin_installed_version": "Decky Plugin (v{{version}})", diff --git a/src/main/events/library/add-custom-game-to-library.ts b/src/main/events/library/add-custom-game-to-library.ts index 6a90087e8..b3da789e7 100644 --- a/src/main/events/library/add-custom-game-to-library.ts +++ b/src/main/events/library/add-custom-game-to-library.ts @@ -9,7 +9,8 @@ const addCustomGameToLibrary = async ( executablePath: string, iconUrl?: string, logoImageUrl?: string, - libraryHeroImageUrl?: string + libraryHeroImageUrl?: string, + coverImageUrl?: string ) => { const objectId = randomUUID(); const shop: GameShop = "custom"; @@ -36,7 +37,7 @@ const addCustomGameToLibrary = async ( libraryImageUrl: iconUrl || "", logoImageUrl: logoImageUrl || "", logoPosition: null, - coverImageUrl: iconUrl || "", + coverImageUrl: coverImageUrl || "", downloadSources: [], }; await gamesShopAssetsSublevel.put(gameKey, assets); @@ -46,6 +47,7 @@ const addCustomGameToLibrary = async ( iconUrl: iconUrl || null, logoImageUrl: logoImageUrl || null, libraryHeroImageUrl: libraryHeroImageUrl || null, + coverImageUrl: coverImageUrl || null, objectId, shop, remoteId: null, diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 4fdeae304..71eaedcdc 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -32,6 +32,7 @@ const addGameToLibrary = async ( iconUrl: gameAssets?.iconUrl ?? null, libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null, logoImageUrl: gameAssets?.logoImageUrl ?? null, + coverImageUrl: gameAssets?.libraryImageUrl ?? null, objectId, shop, remoteId: null, diff --git a/src/main/events/library/cleanup-unused-assets.ts b/src/main/events/library/cleanup-unused-assets.ts index d1d77e9ff..bc9d3d9a8 100644 --- a/src/main/events/library/cleanup-unused-assets.ts +++ b/src/main/events/library/cleanup-unused-assets.ts @@ -38,6 +38,9 @@ const getUsedAssetPaths = async (): Promise> => { if (game.libraryHeroImageUrl?.startsWith("local:")) { usedPaths.add(game.libraryHeroImageUrl.replace("local:", "")); } + if (game.coverImageUrl?.startsWith("local:")) { + usedPaths.add(game.coverImageUrl.replace("local:", "")); + } }); return usedPaths; diff --git a/src/main/events/library/copy-custom-game-asset.ts b/src/main/events/library/copy-custom-game-asset.ts index 1f5aea0f5..85f4dc660 100644 --- a/src/main/events/library/copy-custom-game-asset.ts +++ b/src/main/events/library/copy-custom-game-asset.ts @@ -7,7 +7,7 @@ import { ASSETS_PATH } from "@main/constants"; const copyCustomGameAsset = async ( _event: Electron.IpcMainInvokeEvent, sourcePath: string, - assetType: "icon" | "logo" | "hero" + assetType: "icon" | "logo" | "hero" | "cover" ): Promise => { if (!sourcePath || !fs.existsSync(sourcePath)) { throw new Error("Source file does not exist"); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index 6265791f2..bea5cbd61 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -84,6 +84,7 @@ const getLibrary = async (): Promise => { customIconUrl: game.customIconUrl, customLogoImageUrl: game.customLogoImageUrl, customHeroImageUrl: game.customHeroImageUrl, + customCoverImageUrl: game.customCoverImageUrl, }; }) ); diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 95133c70a..ebc767811 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -9,8 +9,18 @@ const collectAssetPathsToDelete = (game: Game): string[] => { const assetUrls = game.shop === "custom" - ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] - : [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl]; + ? [ + game.iconUrl, + game.logoImageUrl, + game.libraryHeroImageUrl, + game.coverImageUrl, + ] + : [ + game.customIconUrl, + game.customLogoImageUrl, + game.customHeroImageUrl, + game.customCoverImageUrl, + ]; for (const url of assetUrls) { if (url?.startsWith("local:")) { @@ -33,6 +43,7 @@ const updateGameAsDeleted = async ( customIconUrl: null, customLogoImageUrl: null, customHeroImageUrl: null, + customCoverImageUrl: null, }), }; diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 8129fc57c..107726702 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -11,9 +11,11 @@ interface UpdateCustomGameParams { iconUrl?: string; logoImageUrl?: string; libraryHeroImageUrl?: string; + coverImageUrl?: string; originalIconPath?: string; originalLogoPath?: string; originalHeroPath?: string; + originalCoverPath?: string; } const updateCustomGame = async ( @@ -27,9 +29,11 @@ const updateCustomGame = async ( iconUrl, logoImageUrl, libraryHeroImageUrl, + coverImageUrl, originalIconPath, originalLogoPath, originalHeroPath, + originalCoverPath, } = params; const gameKey = levelKeys.game(shop, objectId); @@ -44,6 +48,7 @@ const updateCustomGame = async ( { existing: existingGame.iconUrl, new: iconUrl }, { existing: existingGame.logoImageUrl, new: logoImageUrl }, { existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl }, + { existing: existingGame.coverImageUrl, new: coverImageUrl }, ]; for (const { existing, new: newUrl } of assetPairs) { @@ -58,9 +63,12 @@ const updateCustomGame = async ( iconUrl: iconUrl || null, logoImageUrl: logoImageUrl || null, libraryHeroImageUrl: libraryHeroImageUrl || null, + coverImageUrl: coverImageUrl || null, originalIconPath: originalIconPath || existingGame.originalIconPath || null, originalLogoPath: originalLogoPath || existingGame.originalLogoPath || null, originalHeroPath: originalHeroPath || existingGame.originalHeroPath || null, + originalCoverPath: + originalCoverPath || existingGame.originalCoverPath || null, }; await gamesSublevel.put(gameKey, updatedGame); @@ -74,7 +82,7 @@ const updateCustomGame = async ( libraryHeroImageUrl: libraryHeroImageUrl || "", libraryImageUrl: iconUrl || "", logoImageUrl: logoImageUrl || "", - coverImageUrl: iconUrl || "", + coverImageUrl: coverImageUrl || "", }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 1f9129015..92b15a91a 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -8,7 +8,8 @@ const collectOldAssetPaths = ( existingGame: Game, customIconUrl?: string | null, customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null + customHeroImageUrl?: string | null, + customCoverImageUrl?: string | null ): string[] => { const oldAssetPaths: string[] = []; @@ -16,6 +17,7 @@ const collectOldAssetPaths = ( { existing: existingGame.customIconUrl, new: customIconUrl }, { existing: existingGame.customLogoImageUrl, new: customLogoImageUrl }, { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl }, + { existing: existingGame.customCoverImageUrl, new: customCoverImageUrl }, ]; for (const { existing, new: newUrl } of assetPairs) { @@ -39,9 +41,11 @@ interface UpdateGameDataParams { customIconUrl?: string | null; customLogoImageUrl?: string | null; customHeroImageUrl?: string | null; + customCoverImageUrl?: string | null; customOriginalIconPath?: string | null; customOriginalLogoPath?: string | null; customOriginalHeroPath?: string | null; + customOriginalCoverPath?: string | null; } const updateGameData = async (params: UpdateGameDataParams): Promise => { @@ -52,9 +56,11 @@ const updateGameData = async (params: UpdateGameDataParams): Promise => { customIconUrl, customLogoImageUrl, customHeroImageUrl, + customCoverImageUrl, customOriginalIconPath, customOriginalLogoPath, customOriginalHeroPath, + customOriginalCoverPath, } = params; const updatedGame = { ...existingGame, @@ -62,9 +68,11 @@ const updateGameData = async (params: UpdateGameDataParams): Promise => { ...(customIconUrl !== undefined && { customIconUrl }), ...(customLogoImageUrl !== undefined && { customLogoImageUrl }), ...(customHeroImageUrl !== undefined && { customHeroImageUrl }), + ...(customCoverImageUrl !== undefined && { customCoverImageUrl }), ...(customOriginalIconPath !== undefined && { customOriginalIconPath }), ...(customOriginalLogoPath !== undefined && { customOriginalLogoPath }), ...(customOriginalHeroPath !== undefined && { customOriginalHeroPath }), + ...(customOriginalCoverPath !== undefined && { customOriginalCoverPath }), }; await gamesSublevel.put(gameKey, updatedGame); @@ -106,9 +114,11 @@ interface UpdateGameCustomAssetsParams { customIconUrl?: string | null; customLogoImageUrl?: string | null; customHeroImageUrl?: string | null; + customCoverImageUrl?: string | null; customOriginalIconPath?: string | null; customOriginalLogoPath?: string | null; customOriginalHeroPath?: string | null; + customOriginalCoverPath?: string | null; } const updateGameCustomAssets = async ( @@ -122,9 +132,11 @@ const updateGameCustomAssets = async ( customIconUrl, customLogoImageUrl, customHeroImageUrl, + customCoverImageUrl, customOriginalIconPath, customOriginalLogoPath, customOriginalHeroPath, + customOriginalCoverPath, } = params; const gameKey = levelKeys.game(shop, objectId); @@ -137,7 +149,8 @@ const updateGameCustomAssets = async ( existingGame, customIconUrl, customLogoImageUrl, - customHeroImageUrl + customHeroImageUrl, + customCoverImageUrl ); const updatedGame = await updateGameData({ @@ -147,9 +160,11 @@ const updateGameCustomAssets = async ( customIconUrl, customLogoImageUrl, customHeroImageUrl, + customCoverImageUrl, customOriginalIconPath, customOriginalLogoPath, customOriginalHeroPath, + customOriginalCoverPath, }); await updateShopAssets(gameKey, title); diff --git a/src/main/helpers/download-game-helper.ts b/src/main/helpers/download-game-helper.ts index fc7793516..250efe1c0 100644 --- a/src/main/helpers/download-game-helper.ts +++ b/src/main/helpers/download-game-helper.ts @@ -34,6 +34,7 @@ export const prepareGameEntry = async ({ iconUrl: gameAssets?.iconUrl ?? null, libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null, logoImageUrl: gameAssets?.logoImageUrl ?? null, + coverImageUrl: gameAssets?.coverImageUrl ?? null, objectId, shop, remoteId: null, diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index 498892c90..44b92b3d0 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -97,6 +97,7 @@ export const mergeWithRemoteGames = async () => { iconUrl: game.iconUrl, libraryHeroImageUrl: game.libraryHeroImageUrl, logoImageUrl: game.logoImageUrl, + coverImageUrl: game.coverImageUrl, lastTimePlayed: game.lastTimePlayed, playTimeInMilliseconds: game.playTimeInMilliseconds, hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime, @@ -114,6 +115,7 @@ export const mergeWithRemoteGames = async () => { // Construct coverImageUrl if not provided by backend (Steam games use predictable pattern) const coverImageUrl = game.coverImageUrl || + game.libraryImageUrl || (game.shop === "steam" ? `https://shared.steamstatic.com/store_item_assets/steam/apps/${game.objectId}/library_600x900_2x.jpg` : null); diff --git a/src/preload/index.ts b/src/preload/index.ts index eee7bd493..f7f380e58 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -167,7 +167,8 @@ contextBridge.exposeInMainWorld("electron", { executablePath: string, iconUrl?: string, logoImageUrl?: string, - libraryHeroImageUrl?: string + libraryHeroImageUrl?: string, + coverImageUrl?: string ) => ipcRenderer.invoke( "addCustomGameToLibrary", @@ -175,11 +176,12 @@ contextBridge.exposeInMainWorld("electron", { executablePath, iconUrl, logoImageUrl, - libraryHeroImageUrl + libraryHeroImageUrl, + coverImageUrl ), copyCustomGameAsset: ( sourcePath: string, - assetType: "icon" | "logo" | "hero" + assetType: "icon" | "logo" | "hero" | "cover" ) => ipcRenderer.invoke("copyCustomGameAsset", sourcePath, assetType), saveTempFile: (fileName: string, fileData: Uint8Array) => ipcRenderer.invoke("saveTempFile", fileName, fileData), @@ -193,9 +195,11 @@ contextBridge.exposeInMainWorld("electron", { iconUrl?: string; logoImageUrl?: string; libraryHeroImageUrl?: string; + coverImageUrl?: string; originalIconPath?: string; originalLogoPath?: string; originalHeroPath?: string; + originalCoverPath?: string; }) => ipcRenderer.invoke("updateCustomGame", params), updateGameCustomAssets: (params: { shop: GameShop; @@ -204,9 +208,11 @@ contextBridge.exposeInMainWorld("electron", { customIconUrl?: string | null; customLogoImageUrl?: string | null; customHeroImageUrl?: string | null; + customCoverImageUrl?: string | null; customOriginalIconPath?: string | null; customOriginalLogoPath?: string | null; customOriginalHeroPath?: string | null; + customOriginalCoverPath?: string | null; }) => ipcRenderer.invoke("updateGameCustomAssets", params), createGameShortcut: ( shop: GameShop, diff --git a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx index f50bd8146..30f11fb5f 100644 --- a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx +++ b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx @@ -71,13 +71,15 @@ export function SidebarAddingCustomGameModal({ const iconUrl = ""; // Don't use gradient for icon const logoImageUrl = ""; // Don't use gradient for logo const libraryHeroImageUrl = generateRandomGradient(); // Only use gradient for hero + const coverImageUrl = ""; // Don't use gradient for cover const newGame = await window.electron.addCustomGameToLibrary( gameNameForSeed, executablePath, iconUrl, logoImageUrl, - libraryHeroImageUrl + libraryHeroImageUrl, + coverImageUrl ); showSuccessToast(t("custom_game_modal_success")); diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index d93fc2258..a2d439854 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -128,7 +128,8 @@ declare global { executablePath: string, iconUrl?: string, logoImageUrl?: string, - libraryHeroImageUrl?: string + libraryHeroImageUrl?: string, + coverImageUrl?: string ) => Promise; updateCustomGame: (params: { shop: GameShop; @@ -137,13 +138,15 @@ declare global { iconUrl?: string; logoImageUrl?: string; libraryHeroImageUrl?: string; + coverImageUrl?: string; originalIconPath?: string; originalLogoPath?: string; originalHeroPath?: string; + originalCoverPath?: string; }) => Promise; copyCustomGameAsset: ( sourcePath: string, - assetType: "icon" | "logo" | "hero" + assetType: "icon" | "logo" | "hero" | "cover" ) => Promise; cleanupUnusedAssets: () => Promise<{ deletedCount: number; @@ -156,9 +159,11 @@ declare global { customIconUrl?: string | null; customLogoImageUrl?: string | null; customHeroImageUrl?: string | null; + customCoverImageUrl?: string | null; customOriginalIconPath?: string | null; customOriginalLogoPath?: string | null; customOriginalHeroPath?: string | null; + customOriginalCoverPath?: string | null; }) => Promise; createGameShortcut: ( shop: GameShop, diff --git a/src/renderer/src/pages/game-details/modals/game-assets-settings.tsx b/src/renderer/src/pages/game-details/modals/game-assets-settings.tsx index f980b5fd1..fa1814071 100644 --- a/src/renderer/src/pages/game-details/modals/game-assets-settings.tsx +++ b/src/renderer/src/pages/game-details/modals/game-assets-settings.tsx @@ -8,7 +8,7 @@ import type { Game, LibraryGame, ShopDetailsWithAssets } from "@types"; import "./game-assets-settings.scss"; -type AssetType = "icon" | "logo" | "hero"; +type AssetType = "icon" | "logo" | "hero" | "cover"; interface ElectronFile extends File { path?: string; @@ -18,30 +18,35 @@ interface GameWithOriginalAssets extends Game { originalIconPath?: string; originalLogoPath?: string; originalHeroPath?: string; + originalCoverPath?: string; } interface LibraryGameWithCustomOriginalAssets extends LibraryGame { customOriginalIconPath?: string; customOriginalLogoPath?: string; customOriginalHeroPath?: string; + customOriginalCoverPath?: string; } interface AssetPaths { icon: string; logo: string; hero: string; + cover: string; } interface AssetUrls { icon: string | null; logo: string | null; hero: string | null; + cover: string | null; } interface RemovedAssets { icon: boolean; logo: boolean; hero: boolean; + cover: boolean; } const VALID_IMAGE_TYPES = [ @@ -58,18 +63,21 @@ const INITIAL_ASSET_PATHS: AssetPaths = { icon: "", logo: "", hero: "", + cover: "", }; const INITIAL_REMOVED_ASSETS: RemovedAssets = { icon: false, logo: false, hero: false, + cover: false, }; const INITIAL_ASSET_URLS: AssetUrls = { icon: null, logo: null, hero: null, + cover: null, }; export interface GameAssetsSettingsProps { @@ -130,16 +138,20 @@ export function GameAssetsSettings({ const heroRemoved = !currentGame.libraryHeroImageUrl && Boolean(gameWithAssets.originalHeroPath); + const coverRemoved = + !currentGame.coverImageUrl && Boolean(gameWithAssets.originalCoverPath); setAssetPaths({ icon: extractLocalPath(currentGame.iconUrl), logo: extractLocalPath(currentGame.logoImageUrl), hero: extractLocalPath(currentGame.libraryHeroImageUrl), + cover: extractLocalPath(currentGame.coverImageUrl), }); setAssetDisplayPaths({ icon: extractLocalPath(currentGame.iconUrl), logo: extractLocalPath(currentGame.logoImageUrl), hero: extractLocalPath(currentGame.libraryHeroImageUrl), + cover: extractLocalPath(currentGame.coverImageUrl), }); setOriginalAssetPaths({ icon: @@ -151,12 +163,16 @@ export function GameAssetsSettings({ hero: gameWithAssets.originalHeroPath || extractLocalPath(currentGame.libraryHeroImageUrl), + cover: + gameWithAssets.originalCoverPath || + extractLocalPath(currentGame.coverImageUrl), }); setRemovedAssets({ icon: iconRemoved, logo: logoRemoved, hero: heroRemoved, + cover: coverRemoved, }); }, [extractLocalPath] @@ -174,16 +190,21 @@ export function GameAssetsSettings({ const heroRemoved = !currentGame.customHeroImageUrl && Boolean(gameWithAssets.customOriginalHeroPath); + const coverRemoved = + !currentGame.customCoverImageUrl && + Boolean(gameWithAssets.customOriginalCoverPath); setAssetPaths({ icon: extractLocalPath(currentGame.customIconUrl), logo: extractLocalPath(currentGame.customLogoImageUrl), hero: extractLocalPath(currentGame.customHeroImageUrl), + cover: extractLocalPath(currentGame.customCoverImageUrl), }); setAssetDisplayPaths({ icon: extractLocalPath(currentGame.customIconUrl), logo: extractLocalPath(currentGame.customLogoImageUrl), hero: extractLocalPath(currentGame.customHeroImageUrl), + cover: extractLocalPath(currentGame.customCoverImageUrl), }); setOriginalAssetPaths({ icon: @@ -195,12 +216,16 @@ export function GameAssetsSettings({ hero: gameWithAssets.customOriginalHeroPath || extractLocalPath(currentGame.customHeroImageUrl), + cover: + gameWithAssets.customOriginalCoverPath || + extractLocalPath(currentGame.customCoverImageUrl), }); setRemovedAssets({ icon: iconRemoved, logo: logoRemoved, hero: heroRemoved, + cover: coverRemoved, }); setDefaultUrls({ @@ -211,6 +236,10 @@ export function GameAssetsSettings({ shopDetails?.assets?.libraryHeroImageUrl || currentGame.libraryHeroImageUrl || null, + cover: + shopDetails?.assets?.coverImageUrl || + currentGame.coverImageUrl || + null, }); }, [extractLocalPath, shopDetails] @@ -263,6 +292,8 @@ export function GameAssetsSettings({ return game.logoImageUrl; case "hero": return game.libraryHeroImageUrl; + case "cover": + return game.coverImageUrl; default: return null; } @@ -436,7 +467,13 @@ export function GameAssetsSettings({ ? `local:${assetPaths.hero}` : currentGame.libraryHeroImageUrl; - return { iconUrl, logoImageUrl, libraryHeroImageUrl }; + const coverImageUrl = removedAssets.cover + ? null + : assetPaths.cover + ? `local:${assetPaths.cover}` + : currentGame.coverImageUrl; + + return { iconUrl, logoImageUrl, libraryHeroImageUrl, coverImageUrl }; }; const prepareNonCustomGameAssets = () => { @@ -455,15 +492,21 @@ export function GameAssetsSettings({ ? `local:${assetPaths.hero}` : null; + const customCoverImageUrl = + !removedAssets.cover && assetPaths.cover + ? `local:${assetPaths.cover}` + : null; + return { customIconUrl, customLogoImageUrl, customHeroImageUrl, + customCoverImageUrl, }; }; const updateCustomGame = async (currentGame: LibraryGame | Game) => { - const { iconUrl, logoImageUrl, libraryHeroImageUrl } = + const { iconUrl, logoImageUrl, libraryHeroImageUrl, coverImageUrl } = prepareCustomGameAssets(currentGame); return window.electron.updateCustomGame({ @@ -473,15 +516,21 @@ export function GameAssetsSettings({ iconUrl: iconUrl || undefined, logoImageUrl: logoImageUrl || undefined, libraryHeroImageUrl: libraryHeroImageUrl || undefined, + coverImageUrl: coverImageUrl || undefined, originalIconPath: originalAssetPaths.icon || undefined, originalLogoPath: originalAssetPaths.logo || undefined, originalHeroPath: originalAssetPaths.hero || undefined, + originalCoverPath: originalAssetPaths.cover || undefined, }); }; const updateNonCustomGame = async (currentGame: LibraryGame) => { - const { customIconUrl, customLogoImageUrl, customHeroImageUrl } = - prepareNonCustomGameAssets(); + const { + customIconUrl, + customLogoImageUrl, + customHeroImageUrl, + customCoverImageUrl, + } = prepareNonCustomGameAssets(); return window.electron.updateGameCustomAssets({ shop: currentGame.shop, @@ -490,6 +539,7 @@ export function GameAssetsSettings({ customIconUrl, customLogoImageUrl, customHeroImageUrl, + customCoverImageUrl, customOriginalIconPath: removedAssets.icon ? undefined : originalAssetPaths.icon || undefined, @@ -499,6 +549,9 @@ export function GameAssetsSettings({ customOriginalHeroPath: removedAssets.hero ? undefined : originalAssetPaths.hero || undefined, + customOriginalCoverPath: removedAssets.cover + ? undefined + : originalAssetPaths.cover || undefined, }); }; @@ -682,6 +735,15 @@ export function GameAssetsSettings({ > {t("edit_game_modal_hero")} + + diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index 490d5f175..0613bd0f5 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -509,6 +509,7 @@ export function GameOptionsModal({ iconUrl: game.iconUrl || undefined, logoImageUrl: game.logoImageUrl || undefined, libraryHeroImageUrl: game.libraryHeroImageUrl || undefined, + coverImageUrl: game.coverImageUrl || undefined, }); } else { await window.electron.updateGameCustomAssets({ diff --git a/src/renderer/src/pages/game-launcher/game-launcher.tsx b/src/renderer/src/pages/game-launcher/game-launcher.tsx index af00c7797..7248f4283 100644 --- a/src/renderer/src/pages/game-launcher/game-launcher.tsx +++ b/src/renderer/src/pages/game-launcher/game-launcher.tsx @@ -129,13 +129,11 @@ export default function GameLauncher() { window.electron.closeGameLauncherWindow(); }; - const normalizedCoverImage = - gameAssets?.coverImageUrl?.replaceAll("\\", "/").trim() || ""; - const fallbackSteamCoverImage = - !normalizedCoverImage && shop === "steam" && objectId - ? `https://shared.steamstatic.com/store_item_assets/steam/apps/${objectId}/library_600x900_2x.jpg` - : ""; - const coverImageSource = normalizedCoverImage || fallbackSteamCoverImage; + const coverImageSource = + game?.customCoverImageUrl || + game?.coverImageUrl || + game?.customIconUrl || + game?.iconUrl; const gameTitle = game?.title ?? gameAssets?.title ?? ""; const playTime = game?.playTimeInMilliseconds ?? 0; const achievementCount = game?.achievementCount ?? 0; 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 56f468975..62c19feed 100644 --- a/src/renderer/src/pages/library/library-game-card-large.tsx +++ b/src/renderer/src/pages/library/library-game-card-large.tsx @@ -81,7 +81,9 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({ [ game.customHeroImageUrl, game.libraryHeroImageUrl, - game.libraryImageUrl, + game.customCoverImageUrl, + game.coverImageUrl, + game.customIconUrl, game.iconUrl, ].filter((url) => !!url && url.trim() !== ""), [game] diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx index b999a2f27..dca006b6a 100644 --- a/src/renderer/src/pages/library/library-game-card.tsx +++ b/src/renderer/src/pages/library/library-game-card.tsx @@ -32,9 +32,9 @@ export const LibraryGameCard = memo(function LibraryGameCard({ useGameCard(game, onContextMenu); const sources = [ - game.customIconUrl, // Level 0 + game.customCoverImageUrl, // Level 0 game.coverImageUrl, // Level 1 - game.libraryImageUrl, // Level 2 + game.customIconUrl, // Level 2 game.iconUrl, // Level 3 ].filter((url) => url && url.trim() !== ""); diff --git a/src/types/level.types.ts b/src/types/level.types.ts index a79da3c93..1169a791b 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -34,17 +34,21 @@ export interface User { export interface Game { title: string; iconUrl: string | null; - libraryHeroImageUrl: string | null; logoImageUrl: string | null; + libraryHeroImageUrl: string | null; + coverImageUrl: string | null; customIconUrl?: string | null; customLogoImageUrl?: string | null; customHeroImageUrl?: string | null; + customCoverImageUrl?: string | null; originalIconPath?: string | null; originalLogoPath?: string | null; originalHeroPath?: string | null; + originalCoverPath?: string | null; customOriginalIconPath?: string | null; customOriginalLogoPath?: string | null; customOriginalHeroPath?: string | null; + customOriginalCoverPath?: string | null; playTimeInMilliseconds: number; unsyncedDeltaPlayTimeInMilliseconds?: number; lastTimePlayed: Date | null;