From 686b8db7135d51cf304372fecc4a4968747cb529 Mon Sep 17 00:00:00 2001 From: Vitalij Ryndin Date: Sun, 14 Dec 2025 01:00:34 +0300 Subject: [PATCH 1/2] Add game covers replacer with country restriction --- _locales/de/messages.json | 3 + _locales/en/messages.json | 4 +- _locales/ru/messages.json | 3 + _locales/uk/messages.json | 3 + icons/applogo.svg | 4 + manifest.json | 2 + options/options.html | 6 +- scripts/community/profile.js | 11 + .../community/profile_gamecovers_injected.js | 237 ++++++++++++++++++ 9 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 icons/applogo.svg create mode 100644 scripts/community/profile_gamecovers_injected.js diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 2b83a46e7..d5a15370f 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -686,6 +686,9 @@ "options_builtin_achievements_csrating": { "message": "Verlauf der CS-Wertung bei den CS2-Errungenschaften anzeigen" }, + "options_profile_gamecovers": { + "message": "Spielcover mit Regionsbeschränkung anzeigen" + }, "boostercreator_available_at_date": { "message": "Du wirst bis zum $available_date$ kein weiteres Booster-Pack für dieses Spiel herstellen können.", "description": "\"Booster Pack\" should match how Steam itself names it at: https://steamcommunity.com/tradingcards/boostercreator/", diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e34c9738c..04c18d974 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -692,7 +692,9 @@ "options_builtin_achievements_csrating": { "message": "Display CS rating history on CS2 achievements page" }, - + "options_profile_gamecovers": { + "message": "Display game covers with country restriction" + }, "boostercreator_available_at_date": { "message": "You will not be able to create another Booster Pack for this game until $available_date$.", "description": "\"Booster Pack\" should match how Steam itself names it at: https://steamcommunity.com/tradingcards/boostercreator/", diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 5d91c2e3e..e4d0da4c5 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -686,6 +686,9 @@ "options_builtin_achievements_csrating": { "message": "Отображать историю рейтинга CS на странице достижений CS2" }, + "options_profile_gamecovers": { + "message": "Отображать обложки игр c региональным ограничением" + }, "boostercreator_available_at_date": { "message": "Вы не сможете создать дополнительный набор карточек этой игры до $available_date$.", "description": "\"Booster Pack\" should match how Steam itself names it at: https://steamcommunity.com/tradingcards/boostercreator/", diff --git a/_locales/uk/messages.json b/_locales/uk/messages.json index 455386b59..a86838043 100644 --- a/_locales/uk/messages.json +++ b/_locales/uk/messages.json @@ -686,6 +686,9 @@ "options_builtin_achievements_csrating": { "message": "Показувати історію рейтингу CS на сторінці досягнень CS2" }, + "options_profile_gamecovers": { + "message": "Показувати обкладинки ігор з регіональним обмеженням" + }, "boostercreator_available_at_date": { "message": "Ви не зможете створити інший комплект для цієї гри до $available_date$.", "description": "\"Booster Pack\" should match how Steam itself names it at: https://steamcommunity.com/tradingcards/boostercreator/", 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 70a7e81b6..b2cc0f51a 100644 --- a/manifest.json +++ b/manifest.json @@ -74,6 +74,7 @@ "icons/steamhunters.svg", "icons/image.svg", "icons/achievements_completed.svg", + "icons/applogo.svg", "styles/appicon.css", "styles/inventory-sidebar.css", @@ -82,6 +83,7 @@ "scripts/community/agecheck_injected.js", "scripts/community/filedetails_award_injected.js", "scripts/community/profile_award_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 fee984514..d2d7c8181 100644 --- a/options/options.html +++ b/options/options.html @@ -292,9 +292,13 @@

+
-
+
diff --git a/scripts/community/profile.js b/scripts/community/profile.js index 345ffe2b6..4240198b0 100644 --- a/scripts/community/profile.js +++ b/scripts/community/profile.js @@ -1,6 +1,7 @@ 'use strict'; GetOption( { + 'profile-gamecovers': true, 'profile-calculator': true, 'enhancement-award-popup-url': true, }, ( items ) => @@ -14,6 +15,16 @@ GetOption( { document.head.appendChild( script ); } + 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..4f9ec01f3 --- /dev/null +++ b/scripts/community/profile_gamecovers_injected.js @@ -0,0 +1,237 @@ +'use strict'; + +( () => +{ + const profilePagesRegex = /^https:\/\/steamcommunity\.com\/(id|profiles)\/[^/]+(\/games(\/[^/]+)?)?\/?$/; + if( !profilePagesRegex.test( window.location.href ) ) + { + return; + } + + // Check if "games" part + const isGamesPage = window.location.href.match( profilePagesRegex )[ 2 ] !== undefined; + + /** @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 {string | undefined} src */ + function CheckValidCoverSrc( src ) + { + if( src === undefined || src === null ) + { + return false; + } + + if( src === fallbackCoverImage ) + { + return true; + } + + // Empty src is equal to the current location + return src !== "" + && src !== window.location.href + // Skip fallback cover from Steam CDN + && !src.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 {number} appId + * @param {HTMLImageElement} img + */ + function StoreCoverImage( appId, img ) + { + img.src = fallbackCoverImage; + + // Handle load error + img.addEventListener( 'error', () => + { + // Rollback + img.src = fallbackCoverImage; + appsImageStore.delete( appId ); + }, { once: true } ); + + appsImageStore.set( appId, img ); + } + + function ParseGamePictureCovers() + { + 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; + } + + const coverSrc = coverImg.getAttribute( 'src' ); + if( CheckValidCoverSrc( coverSrc ) ) + { + continue; + } + + const spanPicture = picture.nextSibling; + if( spanPicture instanceof HTMLSpanElement ) + { + spanPicture.style.display = 'none'; + } + + StoreCoverImage( appId, coverImg ); + } + } + + function ParseGameCoverCapsules() + { + /** @type {NodeListOf} */ + const gameCovers = document.querySelectorAll( 'img.game_capsule' ); + for( const cover of gameCovers ) + { + const src = cover.getAttribute( 'src' ); + if( CheckValidCoverSrc( src ) ) + { + continue; + } + + const parent = cover.parentElement; + if( parent instanceof HTMLAnchorElement ) + { + const appId = GetAppIDFromUrl( parent.href ); + if( appsImageStore.has( appId ) ) + { + continue; + } + + StoreCoverImage( appId, cover ); + } + } + } + + 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( isGamesPage ) + { + ParseGamePictureCovers(); + } + else + { + ParseGameCoverCapsules(); + } + + LoadGameCovers(); + } + + InvokeParseCovers(); + + setInterval( () => + { + InvokeParseCovers(); + }, 10_000 ); +} )(); From ac418f2ea06e0ba09c8b303f51ee02b576a046fa Mon Sep 17 00:00:00 2001 From: Vitalij Ryndin Date: Fri, 19 Dec 2025 21:34:28 +0300 Subject: [PATCH 2/2] fix: validate image --- .../community/profile_gamecovers_injected.js | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/scripts/community/profile_gamecovers_injected.js b/scripts/community/profile_gamecovers_injected.js index 4f9ec01f3..3843467d0 100644 --- a/scripts/community/profile_gamecovers_injected.js +++ b/scripts/community/profile_gamecovers_injected.js @@ -35,24 +35,31 @@ return node[ reactFiberKey ]; } - /** @param {string | undefined} src */ - function CheckValidCoverSrc( src ) + /** @param {HTMLImageElement} image */ + function CheckValidImg( image ) { - if( src === undefined || src === null ) + if( image.complete && image.naturalWidth === 0 ) { return false; } - if( src === fallbackCoverImage ) + 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 src !== "" - && src !== window.location.href + return imageSrc !== "" + && imageSrc !== window.location.href // Skip fallback cover from Steam CDN - && !src.includes( 'public/ssr' ); + && !imageSrc.includes( 'public/ssr' ); } /** @@ -113,8 +120,7 @@ continue; } - const coverSrc = coverImg.getAttribute( 'src' ); - if( CheckValidCoverSrc( coverSrc ) ) + if( CheckValidImg( coverImg ) ) { continue; } @@ -135,8 +141,7 @@ const gameCovers = document.querySelectorAll( 'img.game_capsule' ); for( const cover of gameCovers ) { - const src = cover.getAttribute( 'src' ); - if( CheckValidCoverSrc( src ) ) + if( CheckValidImg( cover ) ) { continue; }