From 3eaf04817e156e876e84b7755ca0267e59e58722 Mon Sep 17 00:00:00 2001 From: Julian Schmitt Date: Sat, 4 Apr 2026 12:53:32 +0200 Subject: [PATCH 1/3] Added functionality to edit the cover image of games --- src/locales/de/translation.json | 8 +- src/locales/en/translation.json | 8 +- .../library/add-custom-game-to-library.ts | 6 +- .../events/library/add-game-to-library.ts | 3 +- .../events/library/cleanup-unused-assets.ts | 3 + src/main/events/library/get-library.ts | 1 + .../library/remove-game-from-library.ts | 7 +- src/main/events/library/update-custom-game.ts | 9 +- .../library/update-game-custom-assets.ts | 19 +++- src/main/helpers/download-game-helper.ts | 1 + .../library-sync/merge-with-remote-games.ts | 16 +-- src/preload/index.ts | 10 +- .../sidebar-adding-custom-game-modal.tsx | 4 +- src/renderer/src/declaration.d.ts | 7 +- .../modals/game-assets-settings.tsx | 100 ++++++++++++++---- .../modals/game-options-modal.tsx | 13 +-- .../src/pages/game-launcher/game-launcher.tsx | 16 +-- .../pages/library/library-game-card-large.tsx | 6 +- .../src/pages/library/library-game-card.tsx | 6 +- src/types/level.types.ts | 6 +- 20 files changed, 182 insertions(+), 67 deletions(-) diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index a3df02118..0dd0140d8 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}})", @@ -1044,4 +1050,4 @@ "friend_request_accepted": "Friend request accepted", "friend_request_refused": "Friend request refused" } -} +} \ No newline at end of file diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 8cafa628b..f3ea8f847 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}})", @@ -1067,4 +1073,4 @@ "friend_request_accepted": "Friend request accepted", "friend_request_refused": "Friend request refused" } -} +} \ No newline at end of file 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..65a4c64fa 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, @@ -44,7 +45,7 @@ const addGameToLibrary = async ( } if (game) { - await createGame(game).catch(() => {}); + await createGame(game).catch(() => { }); AchievementWatcherManager.firstSyncWithRemoteIfNeeded( game.shop, 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/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..adbca0a5b 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -9,8 +9,8 @@ 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 +33,7 @@ const updateGameAsDeleted = async ( customIconUrl: null, customLogoImageUrl: null, customHeroImageUrl: null, + customCoverImageUrl: null, }), }; @@ -85,7 +86,7 @@ const removeGameFromLibrary = async ( } if (game.remoteId) { - HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); + HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => { }); } await deleteAssetFiles(assetPathsToDelete); diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 8129fc57c..7ab79337c 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,11 @@ 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 +81,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..5ca0c3a67 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -18,11 +18,11 @@ type ProfileGame = { const getLocalCollectionIds = ( localGame: | { - collectionIds?: string[]; - } + collectionIds?: string[]; + } | { - collectionId?: string | null; - } + collectionId?: string | null; + } | null | undefined ): string[] => { @@ -66,8 +66,8 @@ export const mergeWithRemoteGames = async () => { if (localGame) { const updatedLastTimePlayed = localGame.lastTimePlayed == null || - (game.lastTimePlayed && - new Date(game.lastTimePlayed) > + (game.lastTimePlayed && + new Date(game.lastTimePlayed) > new Date(localGame.lastTimePlayed)) ? game.lastTimePlayed : localGame.lastTimePlayed; @@ -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); @@ -134,5 +136,5 @@ export const mergeWithRemoteGames = async () => { }); } }) - .catch(() => {}); + .catch(() => { }); }; diff --git a/src/preload/index.ts b/src/preload/index.ts index eee7bd493..89de0e74a 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,7 +176,8 @@ contextBridge.exposeInMainWorld("electron", { executablePath, iconUrl, logoImageUrl, - libraryHeroImageUrl + libraryHeroImageUrl, + coverImageUrl ), copyCustomGameAsset: ( sourcePath: string, @@ -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..ad1e83115 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..29acaaffb 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,9 +138,11 @@ declare global { iconUrl?: string; logoImageUrl?: string; libraryHeroImageUrl?: string; + coverImageUrl?: string; originalIconPath?: string; originalLogoPath?: string; originalHeroPath?: string; + originalCoverPath?: string; }) => Promise; copyCustomGameAsset: ( sourcePath: string, @@ -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..14e16312e 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,21 @@ 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 +164,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 +191,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 +217,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 +237,10 @@ export function GameAssetsSettings({ shopDetails?.assets?.libraryHeroImageUrl || currentGame.libraryHeroImageUrl || null, + cover: + shopDetails?.assets?.coverImageUrl || + currentGame.coverImageUrl || + null, }); }, [extractLocalPath, shopDetails] @@ -263,6 +293,8 @@ export function GameAssetsSettings({ return game.logoImageUrl; case "hero": return game.libraryHeroImageUrl; + case "cover": + return game.coverImageUrl default: return null; } @@ -436,7 +468,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 +493,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,14 +517,16 @@ 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 } = + const { customIconUrl, customLogoImageUrl, customHeroImageUrl, customCoverImageUrl } = prepareNonCustomGameAssets(); return window.electron.updateGameCustomAssets({ @@ -490,6 +536,7 @@ export function GameAssetsSettings({ customIconUrl, customLogoImageUrl, customHeroImageUrl, + customCoverImageUrl, customOriginalIconPath: removedAssets.icon ? undefined : originalAssetPaths.icon || undefined, @@ -499,6 +546,9 @@ export function GameAssetsSettings({ customOriginalHeroPath: removedAssets.hero ? undefined : originalAssetPaths.hero || undefined, + customOriginalCoverPath: removedAssets.cover + ? undefined + : originalAssetPaths.cover || undefined, }); }; @@ -582,15 +632,15 @@ export function GameAssetsSettings({ {(assetPath || (isCustomGame(game) && getOriginalAssetUrl(assetType))) && ( - - )} + + )} } /> @@ -603,9 +653,8 @@ export function GameAssetsSettings({ + + {renderImageSection(selectedAssetType)} ); -} +} \ No newline at end of file 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..d2e8fccfd 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({ @@ -583,12 +584,12 @@ export function GameOptionsModal({ }, ...(shouldShowWinePrefixConfiguration ? [ - { - id: "compatibility" as const, - label: t("settings_category_compatibility"), - icon: , - }, - ] + { + id: "compatibility" as const, + label: t("settings_category_compatibility"), + icon: , + }, + ] : []), { id: "downloads" as const, diff --git a/src/renderer/src/pages/game-launcher/game-launcher.tsx b/src/renderer/src/pages/game-launcher/game-launcher.tsx index af00c7797..13f587feb 100644 --- a/src/renderer/src/pages/game-launcher/game-launcher.tsx +++ b/src/renderer/src/pages/game-launcher/game-launcher.tsx @@ -129,13 +129,7 @@ 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; @@ -268,14 +262,14 @@ export default function GameLauncher() { const backgroundStyle = accentColor ? { - background: `linear-gradient(135deg, ${darkenColor(accentColor, 0.7)} 0%, ${darkenColor(accentColor, 0.8, 0.9)} 50%, ${darkenColor(accentColor, 0.85, 0.8)} 100%)`, - } + background: `linear-gradient(135deg, ${darkenColor(accentColor, 0.7)} 0%, ${darkenColor(accentColor, 0.8, 0.9)} 50%, ${darkenColor(accentColor, 0.85, 0.8)} 100%)`, + } : undefined; const glowStyle = accentColor ? { - background: `radial-gradient(ellipse at top right, ${darkenColor(accentColor, 0.3, 0.15)} 0%, transparent 50%)`, - } + background: `radial-gradient(ellipse at top right, ${darkenColor(accentColor, 0.3, 0.15)} 0%, transparent 50%)`, + } : undefined; return ( 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..6d3239c5d 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] @@ -215,7 +217,7 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({ {Math.round( (unlockedAchievementsCount / (game.achievementCount ?? 1)) * - 100 + 100 )} % diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx index b999a2f27..185aef6af 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() !== ""); @@ -138,7 +138,7 @@ export const LibraryGameCard = memo(function LibraryGameCard({ {Math.round( ((game.unlockedAchievementCount ?? 0) / (game.achievementCount ?? 1)) * - 100 + 100 )} % 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; From 30e3093f855f4b6e9c7186e33afda6ba297ff3cd Mon Sep 17 00:00:00 2001 From: Julian Schmitt Date: Sat, 4 Apr 2026 13:11:43 +0200 Subject: [PATCH 2/3] Fix added missing assetType --- src/main/events/library/copy-custom-game-asset.ts | 2 +- src/preload/index.ts | 2 +- src/renderer/src/declaration.d.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/preload/index.ts b/src/preload/index.ts index 89de0e74a..f7f380e58 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -181,7 +181,7 @@ contextBridge.exposeInMainWorld("electron", { ), 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), diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 29acaaffb..a2d439854 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -146,7 +146,7 @@ declare global { }) => Promise; copyCustomGameAsset: ( sourcePath: string, - assetType: "icon" | "logo" | "hero" + assetType: "icon" | "logo" | "hero" | "cover" ) => Promise; cleanupUnusedAssets: () => Promise<{ deletedCount: number; From 1a93a58a96519e0b4addcdd76af114562f99252b Mon Sep 17 00:00:00 2001 From: Julian Schmitt Date: Sat, 4 Apr 2026 13:18:27 +0200 Subject: [PATCH 3/3] code formatted --- src/locales/de/translation.json | 2 +- src/locales/en/translation.json | 2 +- .../events/library/add-game-to-library.ts | 2 +- .../library/remove-game-from-library.ts | 16 +++++-- src/main/events/library/update-custom-game.ts | 3 +- .../library-sync/merge-with-remote-games.ts | 14 +++--- .../sidebar-adding-custom-game-modal.tsx | 2 +- .../modals/game-assets-settings.tsx | 46 +++++++++++-------- .../modals/game-options-modal.tsx | 12 ++--- .../src/pages/game-launcher/game-launcher.tsx | 14 ++++-- .../pages/library/library-game-card-large.tsx | 2 +- .../src/pages/library/library-game-card.tsx | 2 +- 12 files changed, 69 insertions(+), 48 deletions(-) diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 0dd0140d8..389116e9c 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -1050,4 +1050,4 @@ "friend_request_accepted": "Friend request accepted", "friend_request_refused": "Friend request refused" } -} \ No newline at end of file +} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index f3ea8f847..b67e1599b 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1073,4 +1073,4 @@ "friend_request_accepted": "Friend request accepted", "friend_request_refused": "Friend request refused" } -} \ No newline at end of file +} diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 65a4c64fa..71eaedcdc 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -45,7 +45,7 @@ const addGameToLibrary = async ( } if (game) { - await createGame(game).catch(() => { }); + await createGame(game).catch(() => {}); AchievementWatcherManager.firstSyncWithRemoteIfNeeded( game.shop, diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index adbca0a5b..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.coverImageUrl] - : [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl, game.customCoverImageUrl]; + ? [ + 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:")) { @@ -86,7 +96,7 @@ const removeGameFromLibrary = async ( } if (game.remoteId) { - HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => { }); + HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); } await deleteAssetFiles(assetPathsToDelete); diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 7ab79337c..107726702 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -67,7 +67,8 @@ const updateCustomGame = async ( originalIconPath: originalIconPath || existingGame.originalIconPath || null, originalLogoPath: originalLogoPath || existingGame.originalLogoPath || null, originalHeroPath: originalHeroPath || existingGame.originalHeroPath || null, - originalCoverPath: originalCoverPath || existingGame.originalCoverPath || null, + originalCoverPath: + originalCoverPath || existingGame.originalCoverPath || null, }; await gamesSublevel.put(gameKey, updatedGame); 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 5ca0c3a67..44b92b3d0 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -18,11 +18,11 @@ type ProfileGame = { const getLocalCollectionIds = ( localGame: | { - collectionIds?: string[]; - } + collectionIds?: string[]; + } | { - collectionId?: string | null; - } + collectionId?: string | null; + } | null | undefined ): string[] => { @@ -66,8 +66,8 @@ export const mergeWithRemoteGames = async () => { if (localGame) { const updatedLastTimePlayed = localGame.lastTimePlayed == null || - (game.lastTimePlayed && - new Date(game.lastTimePlayed) > + (game.lastTimePlayed && + new Date(game.lastTimePlayed) > new Date(localGame.lastTimePlayed)) ? game.lastTimePlayed : localGame.lastTimePlayed; @@ -136,5 +136,5 @@ export const mergeWithRemoteGames = async () => { }); } }) - .catch(() => { }); + .catch(() => {}); }; 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 ad1e83115..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 @@ -79,7 +79,7 @@ export function SidebarAddingCustomGameModal({ iconUrl, logoImageUrl, libraryHeroImageUrl, - coverImageUrl, + coverImageUrl ); showSuccessToast(t("custom_game_modal_success")); 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 14e16312e..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 @@ -139,8 +139,7 @@ export function GameAssetsSettings({ !currentGame.libraryHeroImageUrl && Boolean(gameWithAssets.originalHeroPath); const coverRemoved = - !currentGame.coverImageUrl && - Boolean(gameWithAssets.originalCoverPath); + !currentGame.coverImageUrl && Boolean(gameWithAssets.originalCoverPath); setAssetPaths({ icon: extractLocalPath(currentGame.iconUrl), @@ -294,7 +293,7 @@ export function GameAssetsSettings({ case "hero": return game.libraryHeroImageUrl; case "cover": - return game.coverImageUrl + return game.coverImageUrl; default: return null; } @@ -526,8 +525,12 @@ export function GameAssetsSettings({ }; const updateNonCustomGame = async (currentGame: LibraryGame) => { - const { customIconUrl, customLogoImageUrl, customHeroImageUrl, customCoverImageUrl } = - prepareNonCustomGameAssets(); + const { + customIconUrl, + customLogoImageUrl, + customHeroImageUrl, + customCoverImageUrl, + } = prepareNonCustomGameAssets(); return window.electron.updateGameCustomAssets({ shop: currentGame.shop, @@ -632,15 +635,15 @@ export function GameAssetsSettings({ {(assetPath || (isCustomGame(game) && getOriginalAssetUrl(assetType))) && ( - - )} + + )} } /> @@ -653,8 +656,9 @@ export function GameAssetsSettings({