diff --git a/_locales/de/messages.json b/_locales/de/messages.json
index c030023ff..81405221e 100644
--- a/_locales/de/messages.json
+++ b/_locales/de/messages.json
@@ -683,6 +683,9 @@
"options_builtin_achievements_csrating": {
"message": "Verlauf der CS-Wertung bei den CS2-Errungenschaften anzeigen"
},
+ "options_profile_gamecovers": {
+ "message": "Spielcover mit Regionsbeschränkung anzeigen"
+ },
"open_desktop_app": {
"message": "In Desktop-App öffnen"
},
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 36de0eb92..a441f8b34 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -701,6 +701,9 @@
"options_builtin_achievements_csrating": {
"message": "Display CS rating history on CS2 achievements page"
},
+ "options_profile_gamecovers": {
+ "message": "Display game covers with country restriction"
+ },
"open_desktop_app": {
"message": "Open in Desktop App"
},
diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json
index 9eadce53e..ec12e9585 100644
--- a/_locales/ru/messages.json
+++ b/_locales/ru/messages.json
@@ -683,6 +683,9 @@
"options_builtin_achievements_csrating": {
"message": "Отображать историю рейтинга CS на странице достижений CS2"
},
+ "options_profile_gamecovers": {
+ "message": "Отображать обложки игр c региональным ограничением"
+ },
"open_desktop_app": {
"message": "Открыть в приложении"
},
diff --git a/_locales/uk/messages.json b/_locales/uk/messages.json
index 37b8c2f67..cc5b847f8 100644
--- a/_locales/uk/messages.json
+++ b/_locales/uk/messages.json
@@ -683,6 +683,9 @@
"options_builtin_achievements_csrating": {
"message": "Показувати історію рейтингу CS на сторінці досягнень CS2"
},
+ "options_profile_gamecovers": {
+ "message": "Показувати обкладинки ігор з регіональним обмеженням"
+ },
"open_desktop_app": {
"message": "Відкрити в програмі"
},
diff --git a/icons/applogo.svg b/icons/applogo.svg
new file mode 100644
index 000000000..a68bd65c4
--- /dev/null
+++ b/icons/applogo.svg
@@ -0,0 +1,4 @@
+
diff --git a/manifest.json b/manifest.json
index 2c61630c0..774677ddf 100644
--- a/manifest.json
+++ b/manifest.json
@@ -67,12 +67,14 @@
"icons/steamhunters.svg",
"icons/image.svg",
"icons/achievements_completed.svg",
+ "icons/applogo.svg",
"styles/appicon.css",
"styles/inventory-sidebar.css",
"scripts/community/inventory.js",
"scripts/community/agecheck_injected.js",
+ "scripts/community/profile_gamecovers_injected.js",
"scripts/community/tradeoffer_injected.js",
"scripts/community/boostercreator_injected.js",
"scripts/store/app_collapse_long_strings.js",
diff --git a/options/options.html b/options/options.html
index a8f9f3547..ad5418bd4 100644
--- a/options/options.html
+++ b/options/options.html
@@ -289,9 +289,13 @@
+
diff --git a/scripts/community/profile.js b/scripts/community/profile.js
index cd62c685b..030e098fd 100644
--- a/scripts/community/profile.js
+++ b/scripts/community/profile.js
@@ -1,9 +1,20 @@
'use strict';
GetOption( {
+ 'profile-gamecovers': true,
'profile-calculator': true,
}, ( items ) =>
{
+ if( items[ 'profile-gamecovers' ] )
+ {
+ const script = document.createElement( 'script' );
+ script.id = 'steamdb_profile_gamecovers';
+ script.type = 'text/javascript';
+ script.dataset.appLogo = GetLocalResource( 'icons/applogo.svg' );
+ script.src = GetLocalResource( 'scripts/community/profile_gamecovers_injected.js' );
+ document.head.appendChild( script );
+ }
+
if( !items[ 'profile-calculator' ] )
{
return;
diff --git a/scripts/community/profile_gamecovers_injected.js b/scripts/community/profile_gamecovers_injected.js
new file mode 100644
index 000000000..0ef9c23ea
--- /dev/null
+++ b/scripts/community/profile_gamecovers_injected.js
@@ -0,0 +1,279 @@
+'use strict';
+
+( () =>
+{
+ const profilePagesRegex = /^https:\/\/steamcommunity\.com\/(id|profiles)\/[^/]+(\/games(\/[^/]+)?|\/home)?\/?$/;
+ if( !profilePagesRegex.test( window.location.href ) )
+ {
+ return;
+ }
+
+ // Check if "games" part
+ const profilePath = window.location.href.match( profilePagesRegex )[ 2 ];
+ const isGamesOrHome = profilePath !== undefined
+ && ( profilePath.includes( 'games' ) || profilePath.includes( 'home' ) );
+
+ /** @type {HTMLScriptElement} */
+ const currentScript = document.querySelector( '#steamdb_profile_gamecovers' );
+ const fallbackCoverImage = currentScript.dataset.appLogo;
+
+ /** @type {Map} */
+ const appsImageStore = new Map();
+
+ /**
+ * @param {string} url
+ * @returns {number}
+ */
+ function GetAppIDFromUrl( url )
+ {
+ const appid = url.match( /\/(?:app|sub|bundle|friendsthatplay|gamecards|recommended|widget)\/(?[0-9]+)/ );
+ return appid ? Number.parseInt( appid.groups.id, 10 ) : -1;
+ }
+
+ /** @param {Record} node */
+ function GetReactFiber( node )
+ {
+ const reactFiberKey = Object.keys( node ).find( key => key.startsWith( '__reactFiber' ) );
+ return node[ reactFiberKey ];
+ }
+
+ /** @param {HTMLImageElement} image */
+ function CheckValidImg( image )
+ {
+ if( image.complete && image.naturalWidth === 0 )
+ {
+ return false;
+ }
+
+ const imageSrc = image.src;
+
+ if( imageSrc === undefined || imageSrc === null )
+ {
+ return false;
+ }
+
+ if( imageSrc === fallbackCoverImage )
+ {
+ return true;
+ }
+
+ // Empty src is equal to the current location
+ return imageSrc !== ""
+ && imageSrc !== window.location.href
+ // Skip fallback cover from Steam CDN
+ && !imageSrc.includes( 'public/ssr' );
+ }
+
+ /**
+ * @param {number} appId
+ * @param {string} path
+ * @returns {string}
+ */
+ function GetCoverUrl( appId, path )
+ {
+ return `https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/${appId}/${path}?t=${Date.now()}`;
+ }
+
+ /**
+ * @param {HTMLImageElement} img
+ */
+ function SetFallbackCoverImage( img )
+ {
+ img.src = fallbackCoverImage;
+ img.style.objectFit = 'cover';
+ }
+
+ /**
+ * @param {number} appId
+ * @param {HTMLImageElement} img
+ */
+ function StoreCoverImage( appId, img )
+ {
+ SetFallbackCoverImage( img );
+
+ img.addEventListener( 'load', () =>
+ {
+ img.style.objectFit = '';
+ }, { once: true } );
+
+ // Handle load error
+ img.addEventListener( 'error', () =>
+ {
+ // Rollback
+ SetFallbackCoverImage( img );
+ appsImageStore.delete( appId );
+ }, { once: true } );
+
+ appsImageStore.set( appId, img );
+ }
+
+ function ParseProfileGameCovers()
+ {
+ const gamePictures = document.querySelectorAll( 'picture' );
+ for( const picture of gamePictures )
+ {
+ const fiber = GetReactFiber( picture );
+ if( !fiber )
+ {
+ continue;
+ }
+
+ const gameProps = fiber.return.memoizedProps?.game;
+ if( !gameProps )
+ {
+ continue;
+ }
+
+ const appId = gameProps.appid;
+ if( appsImageStore.has( appId ) )
+ {
+ continue;
+ }
+
+ const coverImg = picture.querySelector( 'img' );
+ if( !coverImg )
+ {
+ continue;
+ }
+
+ if( CheckValidImg( coverImg ) )
+ {
+ continue;
+ }
+
+ const spanPicture = picture.nextSibling;
+ if( spanPicture instanceof HTMLSpanElement )
+ {
+ spanPicture.style.display = 'none';
+ }
+
+ StoreCoverImage( appId, coverImg );
+ }
+ }
+
+ function ParseProfileCovers()
+ {
+ /** @type {NodeListOf} */
+ const gameCovers = document.querySelectorAll( 'img.game_capsule' );
+ for( const cover of gameCovers )
+ {
+ if( CheckValidImg( cover ) )
+ {
+ continue;
+ }
+
+ const parent = cover.parentElement;
+ if( parent instanceof HTMLAnchorElement )
+ {
+ const appId = GetAppIDFromUrl( parent.href );
+ if( appsImageStore.has( appId ) )
+ {
+ continue;
+ }
+
+ StoreCoverImage( appId, cover );
+ }
+ }
+ }
+
+ function ParseProfileActivityCovers()
+ {
+ /** @type {NodeListOf} */
+ const logos = document.querySelectorAll( '.blotter_gamepurchase_logo > img[src=""]' );
+ for( const logo of logos )
+ {
+ const parent = logo.parentElement;
+ if( parent instanceof HTMLAnchorElement )
+ {
+ const appId = GetAppIDFromUrl( parent.href );
+ if( appsImageStore.has( appId ) )
+ {
+ continue;
+ }
+
+ StoreCoverImage( appId, logo );
+ }
+ }
+ }
+
+ async function LoadGameCovers()
+ {
+ if( appsImageStore.size === 0 )
+ {
+ return;
+ }
+
+ const appIds = Array.from( appsImageStore.keys() ).slice( 0, 50 );
+ console.log( '[SteamDB]: Loading apps', appIds );
+
+ // https://api.steampowered.com/IStoreBrowseService/GetItems/v1/?input_json={"ids":[{"appid":"654310"},{"appid":"1091500"},{"appid":"730"}],"context":{"country_code":"US"},"data_request":{"include_assets":true}}
+ const url = new URL( 'https://api.steampowered.com/IStoreBrowseService/GetItems/v1/' );
+ url.searchParams.set( 'input_json', JSON.stringify( {
+ ids: appIds.slice( 0, 50 ).map( ( appid ) => ( { appid } ) ),
+ context: {
+ country_code: 'US',
+ },
+ data_request: {
+ include_assets: true,
+ },
+ } ) );
+
+ try
+ {
+ const req = await fetch( url , {
+ headers: {
+ 'X-Requested-With': 'SteamDB',
+ },
+ } );
+
+ const res = await req.json();
+
+ for( const item of res.response.store_items )
+ {
+ const appImage = appsImageStore.get( item.appid );
+ if( !appImage )
+ {
+ continue;
+ }
+
+ const headerAsset = item.assets?.header;
+ if( headerAsset )
+ {
+ appImage.src = GetCoverUrl( item.appid, headerAsset );
+ }
+ else
+ {
+ appImage.src = GetCoverUrl( item.appid, 'header.jpg' );
+ }
+
+ appsImageStore.delete( item.appid );
+ }
+ }
+ catch( err )
+ {
+ console.error( '[SteamDB]: Failed to load app', err );
+ }
+ }
+
+ function InvokeParseCovers()
+ {
+ if( isGamesOrHome )
+ {
+ ParseProfileGameCovers();
+ ParseProfileActivityCovers();
+ }
+ else
+ {
+ ParseProfileCovers();
+ }
+
+ LoadGameCovers();
+ }
+
+ InvokeParseCovers();
+
+ setInterval( () =>
+ {
+ InvokeParseCovers();
+ }, 5_000 );
+} )();