Tüm .config dosyaları ilk yedekleme

This commit is contained in:
2026-03-28 03:21:14 +03:00
commit 4f7e8904be
7835 changed files with 1631041 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
'use strict';
GetOption( {
'enhancement-appicon': true,
}, ( items ) =>
{
if( items[ 'enhancement-appicon' ] )
{
let styleAdded = false;
const style = document.createElement( 'link' );
style.id = 'steamdb_appicon';
style.type = 'text/css';
style.rel = 'stylesheet';
style.href = GetLocalResource( 'styles/appicon.css' );
if( document.head )
{
styleAdded = true;
document.head.appendChild( style );
}
window.addEventListener( 'DOMContentLoaded', () =>
{
if( !styleAdded )
{
document.head.appendChild( style );
}
/** @type {HTMLImageElement} */
const icon = document.querySelector( '.apphub_AppIcon > img' );
if( !icon )
{
return;
}
const src = icon.getAttribute( 'src' );
if( !src.includes( '%CDN_HOST_MEDIA_SSL%' ) )
{
return;
}
const applicationConfigElement = document.getElementById( 'application_config' );
if( !applicationConfigElement )
{
return;
}
const applicationConfig = JSON.parse( applicationConfigElement.dataset.config );
icon.src = src.replace( 'https://%CDN_HOST_MEDIA_SSL%/', applicationConfig.MEDIA_CDN_URL );
} );
}
} );

View File

@@ -0,0 +1,775 @@
'use strict';
/** @type {string|null} */
let storeSessionId = null;
/** @type {Record<string, any>|null} */
let userDataCache = null;
/** @type {Record<string, any>|null} */
let userFamilyDataCache = null;
/** @type {Promise<{data: Record<string, any>}|{error: string, data?: Record<string, any>}>|null} */
let userFamilySemaphore = null;
let nextAllowedRequest = 0;
/** @type {chrome|browser} ExtensionApi */
const ExtensionApi = ( () =>
{
if( typeof browser !== 'undefined' && typeof browser.storage !== 'undefined' )
{
return browser;
}
else if( typeof chrome !== 'undefined' && typeof chrome.storage !== 'undefined' )
{
return chrome;
}
throw new Error( 'Did not find appropriate web extensions api' );
} )();
ExtensionApi.runtime.onInstalled.addListener( ( event ) =>
{
if( event.reason === ExtensionApi.runtime.OnInstalledReason.INSTALL )
{
ExtensionApi.tabs.create( {
url: ExtensionApi.runtime.getURL( 'options/options.html' ) + '?welcome=1',
} );
}
} );
ExtensionApi.runtime.onMessage.addListener( ( request, sender, callback ) =>
{
if( !sender || !sender.tab )
{
return false;
}
if( !Object.hasOwn( request, 'contentScriptQuery' ) )
{
return false;
}
switch( request.contentScriptQuery )
{
case 'InvalidateCache': InvalidateCache(); callback(); return true;
case 'FetchSteamUserData': FetchSteamUserData( callback ); return true;
case 'FetchSteamUserFamilyData': FetchSteamUserFamilyData( callback ); return true;
case 'GetApp': GetApp( request.appid, callback ); return true;
case 'GetAppPrice': GetAppPrice( request, callback ); return true;
case 'GetAchievementsGroups': GetAchievementsGroups( request.appid, callback ); return true;
case 'StoreWishlistAdd': StoreWishlistAdd( request.appid, callback ); return true;
case 'StoreWishlistRemove': StoreWishlistRemove( request.appid, callback ); return true;
case 'StoreFollow': StoreFollow( request.appid, callback ); return true;
case 'StoreUnfollow': StoreUnfollow( request.appid, callback ); return true;
case 'StoreIgnore': StoreIgnore( request.appid, callback ); return true;
case 'StoreUnignore': StoreUnignore( request.appid, callback ); return true;
case 'StoreAddToCart': StoreAddToCart( request, callback ); return true;
case 'StoreAddFreeLicense': StoreAddFreeLicense( request, callback ); return true;
case 'StoreRemoveFreeLicense': StoreRemoveFreeLicense( request, callback ); return true;
case 'StoreRequestPlaytestAccess': StoreRequestPlaytestAccess( request, callback ); return true;
}
callback( { success: false, error: 'Unknown query' } );
return false;
} );
function InvalidateCache()
{
userDataCache = null;
userFamilyDataCache = null;
SetLocalOption( 'userdata.cached', Date.now() );
SetLocalOption( 'userfamilydata', '{}' );
}
/**
* @param {(obj: {data: Record<string, any>}|{error: string, data?: Record<string, any>}) => void} callback
*/
async function FetchSteamUserData( callback )
{
if( userDataCache !== null )
{
callback( { data: userDataCache } );
return;
}
const now = Date.now();
const cacheData = await GetLocalOption( { 'userdata.cached': now } );
let cacheTime = cacheData[ 'userdata.cached' ];
if( now > cacheTime + 3600000 )
{
await SetLocalOption( 'userdata.cached', now );
cacheTime = now;
}
const params = new URLSearchParams();
params.set( '_', cacheTime );
try
{
const responseFetch = await fetch(
`https://store.steampowered.com/dynamicstore/userdata/?${params.toString()}`,
{
credentials: 'include',
headers: {
// Pretend we're doing a normal navigation request.
// This will trigger login.steampowered.com redirect flow if user has expired cookies.
Accept: 'text/html',
},
}
);
const response = await responseFetch.json();
if( !response || !response.rgOwnedPackages || !response.rgOwnedPackages.length )
{
throw new Error( 'Are you logged on the Steam Store in this browser?' );
}
// Only keep the data we actually need
userDataCache =
{
rgOwnedPackages: response.rgOwnedPackages || [],
rgOwnedApps: response.rgOwnedApps || [],
rgPackagesInCart: response.rgPackagesInCart || [],
rgAppsInCart: response.rgAppsInCart || [],
rgIgnoredApps: response.rgIgnoredApps || {}, // object, not array
rgIgnoredPackages: response.rgIgnoredPackages || {},
rgFollowedApps: response.rgFollowedApps || [],
rgWishlist: response.rgWishlist || [],
};
callback( { data: userDataCache } );
await SetLocalOption( 'userdata.stored', JSON.stringify( userDataCache ) );
}
catch( error )
{
InvalidateCache();
const data = await GetLocalOption( { 'userdata.stored': false } );
/** @type {{error: string, data?: any}} */
const response =
{
error: error instanceof Error ? error.message : String( error ),
};
if( data[ 'userdata.stored' ] )
{
response.data = JSON.parse( data[ 'userdata.stored' ] );
}
callback( response );
}
}
/**
* @param {(obj: {data: Record<string, any>}|{error: string, data?: Record<string, any>}) => void} callback
*/
async function FetchSteamUserFamilyData( callback )
{
if( userFamilyDataCache !== null )
{
callback( { data: userFamilyDataCache } );
return;
}
if( userFamilySemaphore !== null )
{
callback( await userFamilySemaphore );
return;
}
const now = Date.now();
const cacheData = await GetLocalOption( { userfamilydata: false } );
const cache = cacheData.userfamilydata && JSON.parse( cacheData.userfamilydata );
if( cache && cache.cached && cache.data && now < cache.cached + 21600000 )
{
callback( { data: cache.data } );
return;
}
/** @type {{data: Record<string, any>}|{error: string, data?: Record<string, any>}} */
let callbackResponse = null;
let semaphoreResolve = null;
userFamilySemaphore = new Promise( resolve =>
{
semaphoreResolve = resolve;
} );
try
{
const tokenResponseFetch = await fetch(
`https://store.steampowered.com/pointssummary/ajaxgetasyncconfig`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
);
const token = await tokenResponseFetch.json();
if( !token || !token.success || !token.data || !token.data.webapi_token )
{
throw new Error( 'Are you logged on the Steam Store in this browser?' );
}
const paramsSharedLibrary = new URLSearchParams();
paramsSharedLibrary.set( 'access_token', token.data.webapi_token );
paramsSharedLibrary.set( 'family_groupid', '0' ); // family_groupid is ignored
paramsSharedLibrary.set( 'include_excluded', 'true' );
paramsSharedLibrary.set( 'include_free', 'true' );
paramsSharedLibrary.set( 'include_non_games', 'true' );
// the include_own param has no link with its name, if it is false, then it returns only your owned apps.
// if true, it returns your owned apps and the apps from your family.
paramsSharedLibrary.set( 'include_own', 'true' );
const responseFetch = await fetch(
`https://api.steampowered.com/IFamilyGroupsService/GetSharedLibraryApps/v1/?${paramsSharedLibrary.toString()}`,
{
headers: {
Accept: 'application/json',
}
}
);
const response = await responseFetch.json();
if( !response || !response.response || !response.response.apps )
{
throw new Error( 'Is Steam okay?' );
}
const reduced = response.response.apps.reduce( ( /** @type {any} */ data, /** @type {any} */ app ) =>
{
if( !app.owner_steamids.includes( response.response.owner_steamid ) )
{
if( app.exclude_reason === 0 )
{
data.shared.push( app.appid );
}
}
else
{
data.owned.push( app.appid );
}
return data;
}, {
shared: [],
owned: []
} );
userFamilyDataCache =
{
rgFamilySharedApps: reduced.shared,
rgOwnedApps: reduced.owned,
};
callbackResponse =
{
data: userFamilyDataCache
};
callback( callbackResponse );
await SetLocalOption( 'userfamilydata', JSON.stringify( {
data: userFamilyDataCache,
cached: now
} ) );
}
catch( error )
{
callbackResponse =
{
error: error instanceof Error ? error.message : String( error ),
};
if( cache && cache.data )
{
callbackResponse.data = cache.data;
}
callback( callbackResponse );
}
finally
{
// @ts-ignore - this is assigned inside of a promise
semaphoreResolve( callbackResponse );
userFamilySemaphore = null;
}
}
/**
* @param {Response} response
*/
function GetJsonWithStatusCheck( response )
{
if( !response.ok )
{
if( response.status === 429 )
{
let retryAfter = Number.parseInt( response.headers.get( 'Retry-After' ), 10 );
if( Number.isNaN( retryAfter ) || retryAfter < 1 )
{
retryAfter = 60;
}
nextAllowedRequest = Date.now() + ( retryAfter * 1000 ) + ( Math.random() * 10000 );
console.log( 'Rate limited for', retryAfter, 'seconds, retry after', new Date( nextAllowedRequest ) );
}
const e = new Error( `HTTP ${response.status}` );
e.name = 'ServerError';
throw e;
}
return response.json();
}
/**
* @param {string} appid
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function GetApp( appid, callback )
{
if( nextAllowedRequest > 0 && Date.now() < nextAllowedRequest )
{
callback( { success: false, error: 'Rate limited' } );
return;
}
const params = new URLSearchParams();
params.set( 'appid', Number.parseInt( appid, 10 ).toString() );
fetch( `https://extension.steamdb.info/api/ExtensionApp/?${params.toString()}`, {
headers: {
Accept: 'application/json',
'X-Requested-With': 'SteamDB',
},
} )
.then( GetJsonWithStatusCheck )
.then( callback )
.catch( ( error ) => callback( { success: false, error: error.message } ) );
}
/**
* @param {object} obj
* @param {string} obj.appid
* @param {string} obj.currency
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function GetAppPrice( { appid, currency }, callback )
{
if( nextAllowedRequest > 0 && Date.now() < nextAllowedRequest )
{
callback( { success: false, error: 'Rate limited' } );
return;
}
const params = new URLSearchParams();
params.set( 'appid', Number.parseInt( appid, 10 ).toString() );
params.set( 'currency', currency );
fetch( `https://extension.steamdb.info/api/ExtensionAppPrice/?${params.toString()}`, {
headers: {
Accept: 'application/json',
'X-Requested-With': 'SteamDB',
},
} )
.then( GetJsonWithStatusCheck )
.then( callback )
.catch( ( error ) => callback( { success: false, error: error.message } ) );
}
/**
* @param {string} appid
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function GetAchievementsGroups( appid, callback )
{
if( nextAllowedRequest > 0 && Date.now() < nextAllowedRequest )
{
callback( { success: false, error: 'Rate limited' } );
return;
}
const params = new URLSearchParams();
params.set( 'appid', Number.parseInt( appid, 10 ).toString() );
fetch( `https://extension.steamdb.info/api/ExtensionGetAchievements/?${params.toString()}`, {
headers: {
Accept: 'application/json',
'X-Requested-With': 'SteamDB',
},
} )
.then( GetJsonWithStatusCheck )
.then( callback )
.catch( ( error ) => callback( { success: false, error: error.message } ) );
}
/**
* @param {string} appid
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function StoreWishlistAdd( appid, callback )
{
const formData = new FormData();
formData.set( 'appid', Number.parseInt( appid, 10 ).toString() );
ExecuteStoreApiCall( 'api/addtowishlist', formData, callback );
}
/**
* @param {string} appid
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function StoreWishlistRemove( appid, callback )
{
const formData = new FormData();
formData.set( 'appid', Number.parseInt( appid, 10 ).toString() );
ExecuteStoreApiCall( 'api/removefromwishlist', formData, callback );
}
/**
* @param {string} appid
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function StoreFollow( appid, callback )
{
const formData = new FormData();
formData.set( 'appid', Number.parseInt( appid, 10 ).toString() );
ExecuteStoreApiCall( 'explore/followgame/', formData, callback );
}
/**
* @param {string} appid
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function StoreUnfollow( appid, callback )
{
const formData = new FormData();
formData.set( 'appid', Number.parseInt( appid, 10 ).toString() );
formData.set( 'unfollow', '1' );
ExecuteStoreApiCall( 'explore/followgame/', formData, callback );
}
/**
* @param {string} appid
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function StoreIgnore( appid, callback )
{
const formData = new FormData();
formData.set( 'appid', Number.parseInt( appid, 10 ).toString() );
formData.set( 'ignore_reason', '0' );
ExecuteStoreApiCall( 'recommended/ignorerecommendation/', formData, callback );
}
/**
* @param {string} appid
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function StoreUnignore( appid, callback )
{
const formData = new FormData();
formData.set( 'appid', Number.parseInt( appid, 10 ).toString() );
formData.set( 'remove', '1' );
ExecuteStoreApiCall( 'recommended/ignorerecommendation/', formData, callback );
}
/**
* @param {Record<string, string>} request
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function StoreAddToCart( request, callback )
{
const formData = new FormData();
formData.set( 'action', 'add_to_cart' );
if( request.subid )
{
formData.set( 'subid', Number.parseInt( request.subid, 10 ).toString() );
}
else if( request.bundleid )
{
formData.set( 'bundleid', Number.parseInt( request.bundleid, 10 ).toString() );
}
else
{
return;
}
ExecuteStoreApiCall( 'cart/addtocart', formData, callback );
}
/**
* @param {Record<string, string>} request
* @param {(obj: {success: true}|{success: false, error: string, resultCode: number}) => void} callback
*/
function StoreAddFreeLicense( request, callback )
{
/**
* @param {any} response
*/
const freeLicenseResponse = ( response ) =>
{
if( Array.isArray( response ) )
{
// This api returns [] on success
callback( { success: true } );
InvalidateCache();
return;
}
const resultCode = response?.purchaseresultdetail ?? null;
let message;
switch( resultCode )
{
case 5: message = 'Steam says this is an invalid package.'; break;
case 9: message = 'This product is already available in your Steam library.'; break;
case 24: message = 'You do not own the required app.'; break;
case 53: message = 'You got rate limited, try again later.'; break;
default: message = resultCode === null
? `There was a problem adding this product to your account. ${response?.error ?? ''}`
: `There was a problem adding this product to your account. PurchaseResultDetail=${resultCode}`;
}
callback( {
success: false,
error: message,
resultCode,
} );
};
if( request.subid )
{
const subid = Number.parseInt( request.subid, 10 );
const formData = new FormData();
formData.set( 'ajax', 'true' );
ExecuteStoreApiCall( `freelicense/addfreelicense/${subid}`, formData, freeLicenseResponse, true );
}
else if( request.bundleid )
{
const bundleid = Number.parseInt( request.bundleid, 10 );
const formData = new FormData();
formData.set( 'ajax', 'true' );
ExecuteStoreApiCall( `freelicense/addfreebundle/${bundleid}`, formData, freeLicenseResponse, true );
}
}
/**
* @param {Record<string, string>} request
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
*/
function StoreRemoveFreeLicense( request, callback )
{
if( request.subid )
{
const subid = Number.parseInt( request.subid, 10 );
const formData = new FormData();
formData.set( 'packageid', subid.toString() );
ExecuteStoreApiCall( 'account/removelicense', formData, callback );
}
}
/**
* @param {Record<string, string>} request
* @param {(obj: {success: boolean, granted: boolean}) => void} callback
*/
function StoreRequestPlaytestAccess( request, callback )
{
/**
* @param {any} response
*/
const playtestResponse = ( response ) =>
{
if( response?.success )
{
const granted = !!response.granted;
callback( { success: true, granted } );
if( granted )
{
InvalidateCache();
}
}
else
{
callback( { success: false, granted: false } );
}
};
if( request.appid )
{
const formData = new FormData();
ExecuteStoreApiCall( `ajaxrequestplaytestaccess/${Number.parseInt( request.appid, 10 )}`, formData, playtestResponse, true );
}
}
/**
* @param {string} path
* @param {FormData} formData
* @param {(obj: {success: true}|{success: false, error: string}) => void} callback
* @param {boolean} rawCallback
*/
function ExecuteStoreApiCall( path, formData, callback, rawCallback = false )
{
GetStoreSessionID( ( session ) =>
{
if( !session.success )
{
callback( session );
return;
}
formData.set( 'sessionid', session.sessionID );
const url = `https://store.steampowered.com/${path}`;
fetch( url, {
credentials: 'include',
method: 'POST',
body: formData,
headers: {
// Specify that we're doing an AJAX request, which will prevent Steam from
// nuking users' cookies (even though it won't do that for POST requests either way)
'X-Requested-With': 'SteamDB',
},
} )
.then( async( response ) =>
{
// If we get 401 Unauthorized, it's likely that the login cookie has expired,
// if that's the case, requesting page to get sessionid again should go through
// login redirect and set a fresh login cookie.
// Or the sessionid simply changed.
// Except for ajaxrequestplaytestaccess which actually can return 401 as part of the api
if( response.status === 401 && !path.startsWith( 'ajaxrequestplaytestaccess/' ) )
{
storeSessionId = null;
}
// Handle possible family view requirement
if( response.status === 403 )
{
const text = await response.clone().text();
if( text.includes( 'data-featuretarget="parentalunlock"' ) || text.includes( 'data-featuretarget="parentalfeaturerequest"' ) )
{
throw new Error( 'Your account is currently under Family View restrictions. You need to exit Family View by entering your PIN on the Steam store and then retry this action.' );
}
}
return response.json();
} )
.then( ( response ) =>
{
if( rawCallback )
{
callback( response );
return;
}
if( path === 'explore/followgame/' )
{
// This API returns just true/false instead of an object
response = {
success: response === true,
};
}
if( response?.success )
{
callback( { success: true } );
InvalidateCache();
}
else
{
callback( { success: false, error: 'Failed to do the action. Are you logged in on the Steam store?\nThis item may not be available in your country.' } );
}
} )
.catch( ( error ) => callback( { success: false, error: error.message } ) );
} );
}
/**
* @param {(obj: {success: true, sessionID: string}|{success: false, error: string}) => void} callback
*/
function GetStoreSessionID( callback )
{
if( storeSessionId )
{
callback( { success: true, sessionID: storeSessionId } );
return;
}
const url = 'https://store.steampowered.com/account/preferences';
fetch( url, {
credentials: 'include',
headers: {
// We have to specify that we're doing a normal request (as if the user was navigating).
// This will trigger login.steampowered.com redirect flow if user has expired cookies.
Accept: 'text/html',
},
} )
.then( ( response ) => response.text() )
.then( ( response ) =>
{
const session = response.match( /g_sessionID = "(\w+)";/ );
if( session?.[ 1 ] )
{
storeSessionId = session[ 1 ];
callback( { success: true, sessionID: session[ 1 ] } );
}
else
{
callback( {
success: false,
error: response.includes( 'login' )
? 'Failed to fetch sessionid. It does not look like you are logged in to the Steam store.'
: 'Failed to fetch sessionid. Try reloading the page.',
} );
}
} )
.catch( ( error ) => callback( { success: false, error: error.message } ) );
}
/**
* @param {{[key: string]: any}} items
* @returns {Promise<{[key: string]: any}>}
*/
function GetLocalOption( items )
{
return ExtensionApi.storage.local.get( items );
}
/**
* @param {string} option
* @param {any} value
*/
function SetLocalOption( option, value )
{
/** @type {{ [key: string]: any }} */
const obj = {};
obj[ option ] = value;
return ExtensionApi.storage.local.set( obj );
}

View File

@@ -0,0 +1,34 @@
declare var ExtensionApi: typeof browser | typeof chrome;
declare var CurrentAppID: number;
declare function GetCurrentAppID(): number;
declare function GetHomepage(): string;
declare function _t(message: string, substitutions?: string[]): string;
declare function GetLanguage(): string;
declare type GetOptionCallback = (items: { [key: string]: any }) => void;
declare function GetOption(items: { [key: string]: any }, callback: GetOptionCallback): void;
declare function SetOption(option: string, value: any): void;
declare function GetLocalResource(res: string): string;
declare type SendMessageToBackgroundScriptResponse = {
success: boolean;
error?: string;
data?: any;
};
declare type SendMessageToBackgroundScriptCallback = (data: SendMessageToBackgroundScriptResponse | undefined) => void;
declare function SendMessageToBackgroundScript(
message: { contentScriptQuery: string, [key: string]: any },
callback: SendMessageToBackgroundScriptCallback
): void;
declare function WriteLog(...args: any[]): void;

View File

@@ -0,0 +1,140 @@
/* exported _t, ExtensionApi, GetCurrentAppID, GetHomepage, GetOption, GetLanguage, GetLocalResource, SendMessageToBackgroundScript, SetOption, WriteLog */
'use strict';
/** @type {chrome|browser} ExtensionApi */
// eslint-disable-next-line no-var
var ExtensionApi = ( () =>
{
if( typeof browser !== 'undefined' && typeof browser.storage !== 'undefined' )
{
return browser;
}
else if( typeof chrome !== 'undefined' && typeof chrome.storage !== 'undefined' )
{
return chrome;
}
throw new Error( 'Did not find appropriate web extensions api' );
} )();
// exported variable needs to be `var`
/** @type {number|undefined} */
// eslint-disable-next-line no-var
var CurrentAppID;
/**
* @param {string} url
* @returns {number}
*/
function GetAppIDFromUrl( url )
{
const appid = url.match( /\/(?:app|sub|bundle|friendsthatplay|gamecards|recommended|widget)\/(?<id>[0-9]+)/ );
return appid ? Number.parseInt( appid.groups.id, 10 ) : -1;
}
function GetCurrentAppID()
{
if( !CurrentAppID )
{
CurrentAppID = GetAppIDFromUrl( location.pathname );
}
return CurrentAppID;
}
function GetHomepage()
{
return 'https://steamdb.info/';
}
/**
* @param {string} message
* @param {string[]} substitutions
* @returns {string}
*/
function _t( message, substitutions = [] )
{
return ExtensionApi.i18n.getMessage( message, substitutions );
}
function GetLanguage()
{
return ExtensionApi.i18n.getUILanguage();
}
/**
* @callback GetOptionCallback
* @param {{[key: string]: any}} items
*/
/**
* @param {{[key: string]: any}} items
* @param {GetOptionCallback} callback
*/
function GetOption( items, callback )
{
ExtensionApi.storage.sync.get( items ).then( callback );
}
/**
* @param {string} option
* @param {any} value
*/
function SetOption( option, value )
{
/** @type {{ [key: string]: any }} */
const obj = {};
obj[ option ] = value;
ExtensionApi.storage.sync.set( obj );
}
/**
* @param {string} res
*/
function GetLocalResource( res )
{
return ExtensionApi.runtime.getURL( res );
}
/**
* @callback SendMessageToBackgroundScriptCallback
* @param {{success: boolean, error?: string, data?: any}?} data
*/
/**
* @param {{contentScriptQuery: string, [key: string]: any}} message
* @param {SendMessageToBackgroundScriptCallback} callback
*/
function SendMessageToBackgroundScript( message, callback )
{
/** @param {Error} error */
const errorCallback = ( error ) => callback( { success: false, error: error.message } );
try
{
ExtensionApi.runtime
// @ts-ignore - janky type definitions
.sendMessage( message )
.then( callback )
.catch( errorCallback );
}
catch( error )
{
if( error instanceof Error )
{
errorCallback( error );
}
else
{
errorCallback( new Error( String( error ) ) );
}
}
}
function WriteLog( )
{
console.log( '%c[SteamDB]%c', 'color:#2196F3; font-weight:bold;', '', ...arguments );
}

View File

@@ -0,0 +1,2 @@
declare function DoAchievements(isPersonal: boolean): void;
declare function StartViewTransition(callback: ViewTransitionUpdateCallback): void;

View File

@@ -0,0 +1,434 @@
/* global StartViewTransition */
'use strict';
const tierColors =
[
'#b0c3d9',
'#8cc6ff',
'#6a7dff',
'#c166ff',
'#f03cff',
'#eb4b4b',
'#ffd700',
];
/**
* @typedef {object[]} CSRArray
* @property {number} csr
* @property {string} datetime
* @property {string} season
* @property {number} [delta]
*/
/**
* @param {Element} container
* @param {CSRArray} initialData
*/
const InitChart = ( container, initialData ) =>
{
let maxLength = 200;
const canvas = document.createElement( 'canvas' );
canvas.className = 'steamdb_achievements_csrating_graph';
container.append( canvas );
const ctx = canvas.getContext( '2d' );
ctx.font = '16px "Motiva Sans", sans-serif';
const tooltip = document.createElement( 'div' );
tooltip.className = 'community_tooltip steamdb_achievements_csrating_graph_tooltip';
document.body.append( tooltip );
canvas.addEventListener( 'mousemove', ( event ) =>
{
const gap = canvas.offsetWidth / ( Math.min( initialData.length, maxLength ) - 1 );
const x = event.offsetX - ( gap / 2 );
const index = Math.ceil( x / gap );
DrawChart( initialData, index, canvas, tooltip, maxLength );
tooltip.style.display = 'block';
const tooltipWidth = tooltip.clientWidth;
const shiftTooltip = event.pageX + tooltipWidth - document.body.clientWidth;
tooltip.style.left = shiftTooltip > 0
? event.pageX - shiftTooltip + 'px'
: event.pageX + 'px';
tooltip.style.top = event.pageY + 30 + 'px';
} );
const resetCanvas = () =>
{
DrawChart( initialData, -1, canvas, tooltip, maxLength );
tooltip.style.display = 'none';
};
canvas.addEventListener( 'mouseleave', resetCanvas );
window.addEventListener( 'resize', resetCanvas );
maxLength = Math.min( maxLength, initialData.length );
/** @type {HTMLInputElement} */
const maxLengthInput = document.createElement( 'input' );
maxLengthInput.className = 'steamdb_achievements_csrating_graph_slider';
maxLengthInput.type = 'range';
maxLengthInput.min = '2';
maxLengthInput.max = initialData.length.toString();
maxLengthInput.value = maxLength.toString();
maxLengthInput.addEventListener( 'input', () =>
{
maxLength = Number.parseInt( maxLengthInput.value, 10 );
DrawChart( initialData, -1, canvas, tooltip, maxLength );
} );
canvas.insertAdjacentElement( 'afterend', maxLengthInput );
return { canvas, tooltip, maxLength };
};
/**
* @param {CSRArray} initialData
* @param {number} hoveredIndex
* @param {HTMLCanvasElement} canvas
* @param {HTMLDivElement} tooltip
* @param {number} maxLength
*/
const DrawChart = ( initialData, hoveredIndex, canvas, tooltip, maxLength ) =>
{
const data = initialData.slice( 0, maxLength ).reverse();
const maxCSR = data.reduce( ( a, b ) => a.csr > b.csr ? a : b ).csr;
const rect = canvas.getBoundingClientRect();
const width = rect.width * devicePixelRatio;
const height = rect.height * devicePixelRatio;
const ctx = canvas.getContext( '2d' );
// Setting size clears the canvas
canvas.width = width;
canvas.height = height;
// Draw gradient
let i = 0;
let lastTier = -1;
let lastSeason = data[ 0 ].season;
const paddedHeight = height * 0.95;
const halfHeight = height / 2;
const gap = width / ( data.length - 1 );
/** @type {{season: string, x: number}[]} */
const seasonChanges = [];
ctx.beginPath();
ctx.moveTo( 0, height );
for( const point of data )
{
const val = 2 * ( point.csr / maxCSR - 0.5 );
const x = i * gap;
const y = ( -val * paddedHeight ) / 2 + halfHeight;
const tier = Math.min( Math.floor( point.csr / 5000 ), tierColors.length - 1 );
if( lastTier !== tier )
{
if( i > 0 )
{
ctx.lineTo( x, y );
ctx.lineTo( x, height );
ctx.fill();
ctx.beginPath();
ctx.moveTo( x, height );
}
const grd = ctx.createLinearGradient( 0, 0, 0, height );
grd.addColorStop( 0, tierColors[ tier ] + '22' );
grd.addColorStop( 1, 'transparent' );
ctx.fillStyle = grd;
ctx.lineTo( x, y );
lastTier = tier;
}
else
{
ctx.lineTo( x, y );
}
if( lastSeason !== point.season )
{
lastSeason = point.season;
seasonChanges.push( {
x,
season: lastSeason,
} );
}
i += 1;
}
ctx.lineTo( width, height );
ctx.fill();
// Max tier dashed line
ctx.strokeStyle = '#424857';
ctx.lineWidth = 1 * devicePixelRatio;
ctx.setLineDash( [ 7 * devicePixelRatio, 4 * devicePixelRatio ] );
ctx.fillStyle = '#999';
i = 0;
for( let maxCSRClean = maxCSR - ( maxCSR % 5000 ); maxCSRClean >= 5000 && i < 2; maxCSRClean -= 5000, i++ )
{
const maxCSRTier = 2 * ( maxCSRClean / maxCSR - 0.5 );
const maxCSRTierY = ( -maxCSRTier * paddedHeight ) / 2 + halfHeight;
ctx.beginPath();
ctx.moveTo( 0, maxCSRTierY );
ctx.lineTo( width, maxCSRTierY );
ctx.stroke();
ctx.fillText( `${( maxCSRClean / 1000 ).toFixed( 0 )}k`, 0, maxCSRTierY < 12 ? maxCSRTierY + 12 : maxCSRTierY - 4 );
}
ctx.setLineDash( [] );
// Draw season changes
for( const season of seasonChanges )
{
ctx.beginPath();
ctx.moveTo( season.x, 0 );
ctx.lineTo( season.x, height );
ctx.stroke();
ctx.fillText( `Season ${season.season}`, season.x + 5, height - 5 );
}
// Draw line
ctx.beginPath();
ctx.lineWidth = 2 * devicePixelRatio;
let circleX = null;
let circleY = null;
let highlightedCSR = 0;
let highlightedDate = '';
i = 0;
lastTier = -1;
for( const point of data )
{
const val = 2 * ( point.csr / maxCSR - 0.5 );
const x = i * gap;
const y = ( -val * paddedHeight ) / 2 + halfHeight;
const tier = Math.min( Math.floor( point.csr / 5000 ), tierColors.length - 1 );
if( lastTier !== tier )
{
if( i > 0 )
{
ctx.lineTo( x, y );
ctx.stroke();
ctx.beginPath();
}
ctx.strokeStyle = tierColors[ tier ];
ctx.moveTo( x, y );
lastTier = tier;
}
else
{
ctx.lineTo( x, y );
}
if( hoveredIndex === i )
{
circleX = x;
circleY = y;
highlightedCSR = point.csr;
highlightedDate = point.datetime;
}
i += 1;
}
ctx.stroke();
if( circleX !== null && circleY !== null )
{
ctx.beginPath();
ctx.fillStyle = '#fff';
ctx.arc( circleX, circleY, 3 * devicePixelRatio, 0, Math.PI * 2 );
ctx.fill();
tooltip.textContent = `${highlightedCSR.toLocaleString()}\n${highlightedDate}`;
}
};
/**
* @param {Element} container
* @param {CSRArray} rows
*/
const CreateCSRatingTable = ( container, rows ) =>
{
const table = document.createElement( 'table' );
table.className = 'steamdb_achievements_csrating';
container.append( table );
const CreateHeader = () =>
{
const header = document.createElement( 'tr' );
const headerTdDatetime = document.createElement( 'th' );
headerTdDatetime.textContent = _t( 'achievements_csrating_date' );
const headerTdCSR = document.createElement( 'th' );
headerTdCSR.textContent = _t( 'achievements_csrating_name' );
const headerTdCSRdelta = document.createElement( 'th' );
headerTdCSRdelta.textContent = 'Δ';
header.append( headerTdDatetime, headerTdCSR, headerTdCSRdelta );
return header;
};
let prevScore = 0;
for( let i = rows.length - 1; i >= 0; i-- )
{
if( prevScore !== 0 )
{
rows[ i ].delta = rows[ i ].csr - prevScore;
}
prevScore = rows[ i ].csr;
}
const tbody = document.createElement( 'tbody' );
table.append( tbody );
let season;
for( const row of rows )
{
if( season !== row.season )
{
season = row.season;
const tr = document.createElement( 'tr' );
tbody.append( tr );
const th = document.createElement( 'th' );
th.textContent = _t( 'achievements_csrating_season', [ season ] );
th.colSpan = 3;
th.className = 'steamdb_achievements_csrating_season';
tr.append( th );
tbody.append( CreateHeader() );
}
const tr = document.createElement( 'tr' );
tbody.append( tr );
const datetime = document.createElement( 'td' );
datetime.textContent = row.datetime;
tr.append( datetime );
const csr = document.createElement( 'td' );
csr.textContent = row.csr.toLocaleString();
const tier = Math.min( Math.floor( row.csr / 5000 ), tierColors.length - 1 );
csr.className = 'steamdb_achievements_csrating-value';
csr.style.color = tierColors[ tier ];
tr.append( csr );
const delta = document.createElement( 'td' );
if( row.delta )
{
delta.textContent = ( row.delta > 0 ? '+' : '' ) + row.delta;
}
delta.className = row.delta > 0
? 'steamdb_achievements_csrating_positive'
: 'steamdb_achievements_csrating_negative';
if( row.delta < -199 || row.delta > 199 )
{
delta.classList.add( 'steamdb_achievements_csrating_significant' );
}
tr.append( delta );
}
};
/**
* @param {string} profileUrl
*/
const FetchCSRating = async( profileUrl ) =>
{
const res = await fetch( `https://steamcommunity.com${profileUrl}/gcpd/730?tab=majors&ajax=1` );
const json = await res.json();
const parser = new DOMParser();
const dom = parser.parseFromString( json.html, 'text/html' );
const rows = [ ...dom.querySelectorAll( 'tr' ) ]
.filter( tr => tr.querySelector( 'td' )?.textContent.startsWith( 'premier' ) );
const dateFormatter = new Intl.DateTimeFormat( GetLanguage(), {
dateStyle: 'medium',
timeStyle: 'short',
} );
/** @type {CSRArray} */
const premierRows = [];
for( const row of rows )
{
premierRows.push( {
season: row.querySelector( 'td' ).textContent.replace( 'premier_season', '' ),
datetime: dateFormatter.format(
new Date( row.querySelector( 'td:nth-child(2)' ).textContent ).getTime(),
),
csr: Number( row.querySelector( 'td:nth-child(3)' ).textContent ) >> 15,
} );
}
if( premierRows.length < 1 )
{
return;
}
const summary = document.createElement( 'details' );
summary.open = true;
const summaryName = document.createElement( 'summary' );
summaryName.className = 'steamdb_achievements_game_name steamdb_achievements_csrating_fold';
summaryName.textContent = _t( 'achievements_csrating_name' );
summary.append( summaryName );
let chart = null;
if( premierRows.length > 1 )
{
chart = InitChart( summary, premierRows );
}
CreateCSRatingTable( summary, premierRows );
StartViewTransition( () =>
{
document.querySelector( '#mainContents' ).append( summary );
if( chart !== null )
{
DrawChart( premierRows, -1, chart.canvas, chart.tooltip, chart.maxLength );
}
} );
};
/**
* @param {string} str
*/
const removeTrailingSlash = ( str ) => str.endsWith( '/' ) ? str.slice( 0, -1 ) : str;
const viewingProfile = removeTrailingSlash( /** @type {HTMLAnchorElement} */ ( document.querySelector( '.pagecontent .persona_name_text_content' ) )?.pathname ?? '' );
const myProfile = removeTrailingSlash( /** @type {HTMLAnchorElement} */ ( document.querySelector( '#global_actions .user_avatar' ) )?.pathname ?? '' );
if( viewingProfile === myProfile )
{
FetchCSRating( myProfile );
}

View File

@@ -0,0 +1,27 @@
/* global DoAchievements */
'use strict';
DoAchievements( false );
{
/** @type {HTMLAnchorElement} */
const currentUser = document.querySelector( '#global_actions .user_avatar' );
const currentUserPath = location.pathname.split( '/' );
if( currentUser && currentUserPath[ 1 ] === 'stats' )
{
const currentUserUrl = currentUser.href.replace( /\/$/, '' );
const tab = document.createElement( 'div' );
tab.className = 'tab steamdb_stats_tab';
const link = document.createElement( 'a' );
link.className = 'tabOff';
link.href = `${currentUserUrl}/stats/${currentUserPath[ 2 ]}?tab=achievements`;
link.textContent = _t( 'view_your_achievements' );
tab.appendChild( link );
document.querySelector( '#tabs' )?.appendChild( tab );
}
}

View File

@@ -0,0 +1,5 @@
/* global DoAchievements */
'use strict';
DoAchievements( true );

View File

@@ -0,0 +1,21 @@
'use strict';
GetOption( { 'enhancement-skip-agecheck': false }, ( items ) =>
{
if( items[ 'enhancement-skip-agecheck' ] )
{
const element = document.createElement( 'script' );
element.id = 'steamdb_skip_agecheck';
element.type = 'text/javascript';
element.src = GetLocalResource( 'scripts/community/agecheck_injected.js' );
if( document.head )
{
document.head.insertBefore( element, document.head.firstChild );
}
else
{
document.documentElement.appendChild( element );
}
}
} );

View File

@@ -0,0 +1,9 @@
'use strict';
( ( () =>
{
if( 'AcceptAppHub' in window && 'Proceed' in window )
{
window.Proceed();
}
} )() );

View File

@@ -0,0 +1,52 @@
'use strict';
( ( () =>
{
/** @type {HTMLSelectElement} */
const gameSelector = document.querySelector( '#booster_game_selector' );
if( !gameSelector )
{
return;
}
// Add a `div` container to display the booster pack available date
const availableDateContainer = document.createElement( 'div' );
availableDateContainer.id = 'booster_available_date';
gameSelector.after( availableDateContainer );
// Add an event listener to catch the details about the chosen booster pack
// This data is sent by `boostercreator_injected.js` when the game selector changes
gameSelector.addEventListener( 'steamdb-booster-game-change', function( event )
{
/** @type {CustomEvent<{ available_at_time?: string }>} */
const customEvent = /** @type {CustomEvent} */ ( event );
displayBoosterAvailableDate( customEvent.detail?.available_at_time );
} );
/**
* @param {string | undefined} availableDate
*/
function displayBoosterAvailableDate( availableDate )
{
if( availableDate )
{
availableDateContainer.textContent = _t( 'boostercreator_available_at_date', [ availableDate ] );
}
else
{
availableDateContainer.textContent = '';
}
}
// Inject a script into the page, so we can access page Steam variables
setTimeout( () =>
{
const script = document.createElement( 'script' );
script.id = 'steamdb_boostercreator';
script.type = 'text/javascript';
script.src = GetLocalResource( 'scripts/community/boostercreator_injected.js' );
document.head.appendChild( script );
}, 0 );
} )() );

View File

@@ -0,0 +1,21 @@
'use strict';
/** @type {HTMLSelectElement} */
const gameSelector = document.querySelector( '#booster_game_selector' );
if( gameSelector )
{
// Add an event listener to catch when the game selector changes and emit new rgBoosterData
gameSelector.addEventListener( 'change', emitBoosterAvailableDate );
// Emit rgBoosterData for current selection when the script loads
emitBoosterAvailableDate();
}
function emitBoosterAvailableDate( )
{
const selectedGame = gameSelector.value;
const rgBoosterData = window.CBoosterCreatorPage.sm_rgBoosterData[ selectedGame ];
gameSelector.dispatchEvent( new CustomEvent( 'steamdb-booster-game-change', { detail: rgBoosterData } ) );
}

View File

@@ -0,0 +1,15 @@
'use strict';
GetOption( {
'enhancement-award-popup-url': true,
}, ( items ) =>
{
if( items[ 'enhancement-award-popup-url' ] && window.location.search.includes( 'award' ) )
{
const script = document.createElement( 'script' );
script.id = 'steamdb_filedetails_award';
script.type = 'text/javascript';
script.src = GetLocalResource( 'scripts/community/filedetails_award_injected.js' );
document.head.appendChild( script );
}
} );

View File

@@ -0,0 +1,39 @@
'use strict';
( ( () =>
{
if( !( 'PublishedFileAward' in window ) )
{
return;
}
const params = new URLSearchParams( window.location.search );
const awardId = params.get( 'award' );
if( awardId === null )
{
return;
}
const button = document.querySelector( '.general_btn[onClick^="PublishedFileAward"]' );
if( !button )
{
console.log( '[SteamDB] Failed to find PublishedFileAward button' );
return;
}
const data = button.getAttribute( 'onClick' ).match( /PublishedFileAward\(\s*'(?<id>[0-9]+)',\s*(?<fileType>[0-9]+)\s*\)/ );
if( !data )
{
console.log( '[SteamDB] Failed to extract data from PublishedFileAward button' );
return;
}
window.PublishedFileAward(
data.groups.id,
Number.parseInt( data.groups.fileType, 10 ),
Number.parseInt( awardId, 10 ),
);
} )() );

View File

@@ -0,0 +1,63 @@
'use strict';
if( document.querySelector( '.guideTopContent' ) )
{
const guide = document.querySelector( '.guide' );
if( guide.querySelector( '.bb_spoiler' ) )
{
/**
* @param {ViewTransitionUpdateCallback} callback
*/
const StartViewTransition = ( callback ) =>
{
if( document.startViewTransition )
{
document.startViewTransition( () =>
{
try
{
callback();
}
catch( e )
{
console.error( e );
}
} );
}
else
{
callback();
}
};
const controls = document.querySelector( '#ItemControls' );
const divider = document.createElement( 'div' );
divider.className = 'vertical_divider';
controls.append( divider );
const checkboxWrapper = document.createElement( 'label' );
checkboxWrapper.textContent = _t( 'spoilers_reveal' );
checkboxWrapper.className = 'workshopItemControlCtn general_btn steamdb_reveal_spoilers_button';
const checkbox = document.createElement( 'input' );
checkbox.type = 'checkbox';
checkboxWrapper.prepend( checkbox );
controls.append( checkboxWrapper );
checkbox.addEventListener( 'change', () =>
{
const spoilers = guide.querySelectorAll( '.bb_spoiler' );
const reveal = checkbox.checked;
StartViewTransition( () =>
{
for( const spoiler of spoilers )
{
spoiler.classList.toggle( 'steamdb_spoiler_revealed', reveal );
}
} );
} );
}
}

View File

@@ -0,0 +1,97 @@
/* global CurrentAppID: true */
'use strict';
GetOption( {
'button-gamehub': true,
'button-pcgw': true,
}, ( items ) =>
{
const container = document.querySelector( '.apphub_OtherSiteInfo' );
if( container )
{
// Are we in a hacky game group with a custom url?
if( GetCurrentAppID() === -1 )
{
/** @type {HTMLAnchorElement} */
const sectionTab = document.querySelector( '.apphub_sectionTab' );
const match = sectionTab.href.match( /\/([0-9]+)\/?/ );
CurrentAppID = CurrentAppID ? Number.parseInt( match[ 1 ], 10 ) : -1;
}
if( GetCurrentAppID() < 1 )
{
return;
}
// Make in-game number clickable
const numInApp = document.querySelector( '.apphub_NumInApp' );
if( numInApp )
{
const link = document.createElement( 'a' );
link.className = 'apphub_NumInApp';
link.href = GetHomepage() + 'app/' + GetCurrentAppID() + '/charts/';
link.title = _t( 'view_on_steamdb' );
link.textContent = numInApp.textContent;
numInApp.parentNode.replaceChild( link, numInApp );
}
if( items[ 'button-gamehub' ] )
{
const link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_medium btn_steamdb';
link.href = GetHomepage() + 'app/' + GetCurrentAppID() + '/';
const element = document.createElement( 'span' );
element.dataset.tooltipText = _t( 'view_on_steamdb' );
link.appendChild( element );
const image = document.createElement( 'img' );
image.className = 'ico16';
image.src = GetLocalResource( 'icons/white.svg' );
element.appendChild( image );
container.insertBefore( link, container.firstChild );
const responsiveMenu = document.querySelector( '.apphub_ResponsiveMenuCtn' );
if( responsiveMenu )
{
responsiveMenu.append( link.cloneNode( true ) );
}
}
if( items[ 'button-pcgw' ] )
{
const link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_medium btn_steamdb';
link.href = 'https://pcgamingwiki.com/api/appid.php?appid=' + GetCurrentAppID() + '&utm_source=SteamDB';
const element = document.createElement( 'span' );
element.dataset.tooltipText = _t( 'view_on_pcgamingwiki' );
link.appendChild( element );
const image = document.createElement( 'img' );
image.className = 'ico16';
image.src = GetLocalResource( 'icons/pcgamingwiki.svg' );
element.appendChild( image );
container.insertBefore( link, container.firstChild );
container.insertBefore( document.createTextNode( ' ' ), link.nextSibling );
const responsiveMenu = document.querySelector( '.apphub_ResponsiveMenuCtn' );
if( responsiveMenu )
{
responsiveMenu.append( document.createTextNode( ' ' ) );
responsiveMenu.append( link.cloneNode( true ) );
}
}
}
} );

View File

@@ -0,0 +1,821 @@
'use strict';
( ( () =>
{
/** @type {Record<string, {packageid: number, owned: boolean}>} */
const giftCache = {}; // TODO: Store this in indexeddb
const scriptHook = document.getElementById( 'steamdb_inventory_hook' );
const homepage = scriptHook.dataset.homepage;
const logoSrc = scriptHook.dataset.logo;
const optionsUrl = scriptHook.dataset.optionsUrl;
const i18n = JSON.parse( scriptHook.dataset.i18n );
const options = JSON.parse( scriptHook.dataset.options );
const hasLinksEnabled = options[ 'link-inventory' ];
const hasBadgeInfoEnabled = options[ 'enhancement-inventory-badge-info' ];
let hasQuickSellEnabled = options[ 'enhancement-inventory-quick-sell' ] && window.g_bViewingOwnProfile && window.g_bMarketAllowed;
let quickSellHeight = Number.parseInt( getComputedStyle( document.body ).getPropertyValue( '--steamdb-quick-sell-height' ), 10 ) || 0;
/** @type {AbortController | null} */
let currentAbortController = null;
const dummySellEvent =
{
stop: () =>
{
// empty
},
};
/**
* @this {HTMLAnchorElement}
*/
const OnQuickSellButtonClick = function( )
{
window.SellCurrentSelection();
/** @type {HTMLInputElement} */
const inputBuyer = document.querySelector( '#market_sell_buyercurrency_input' );
inputBuyer.value = ( Number.parseFloat( this.dataset.price ) / 100.0 ).toString();
inputBuyer.dispatchEvent( new Event( 'keyup', { bubbles: true } ) );
/** @type {HTMLInputElement} */
const inputSeller = document.querySelector( '#market_sell_currency_input' );
inputSeller.dispatchEvent( new Event( 'keyup', { bubbles: true } ) );
if( options[ 'enhancement-inventory-quick-sell-auto' ] )
{
// SSA must be accepted before OnAccept call, as it has a check for it
/** @type {HTMLInputElement} */
const ssa = document.querySelector( '#market_sell_dialog_accept_ssa' );
ssa.checked = true;
window.SellItemDialog.OnAccept( dummySellEvent );
window.SellItemDialog.OnConfirmationAccept( dummySellEvent );
}
};
const currencyCode = window.GetCurrencyCode( window.g_rgWalletInfo.wallet_currency );
/**
* @param {number} valueInCents
*/
const FormatCurrency = ( valueInCents ) =>
window.v_currencyformat( valueInCents, currencyCode, window.g_rgWalletInfo.wallet_country );
if( options[ 'enhancement-inventory-no-sell-reload' ] )
{
let nextRefreshCausedBySell = false;
const originalOnSuccess = window.SellItemDialog.OnSuccess;
const originalReloadInventory = window.CUserYou.prototype.ReloadInventory;
/**
* @param {any} transport
*/
window.SellItemDialog.OnSuccess = function( transport )
{
nextRefreshCausedBySell = true;
let className = 'listed';
if( transport.responseJSON.requires_confirmation )
{
className = transport.responseJSON.needs_mobile_confirmation ? 'mobile' : 'email';
transport.responseJSON.requires_confirmation = false;
}
window.g_ActiveInventory.selectedItem.element.classList.add( 'steamdb_confirm_' + className );
return originalOnSuccess.apply( this, arguments );
};
window.CUserYou.prototype.ReloadInventory = function( )
{
if( nextRefreshCausedBySell )
{
nextRefreshCausedBySell = false;
window.g_ActiveInventory.selectedItem.element.classList.add( 'steamdb_sold' );
}
else
{
return originalReloadInventory.apply( this, arguments );
}
};
}
const originalRenderItemInfo = window.RenderItemInfo;
/**
* @param {string} name
* @param {any} description
* @param {any} asset
*/
window.RenderItemInfo = function SteamDB_RenderItemInfo( name, description, asset )
{
/*
if( !window.g_bViewingOwnProfile )
{
window.g_bIsTrading = true; // Hides sell button
window.g_bMarketAllowed = true; // Has to be set so Valve's code doesn't try to bind a tooltip on non existing sell button
}
*/
const container = document.getElementById( name );
container.querySelector( 'steamdb-iteminfo-footer' )?.remove();
const originalReturn = originalRenderItemInfo.apply( this, arguments );
if( !description )
{
return originalReturn;
}
try
{
RenderItemInfo( container, description, asset );
}
catch( e )
{
console.error( '[SteamDB] RenderItemInfo error', e );
}
return originalReturn;
};
/**
* @param {HTMLElement} container
* @param {any} description
* @param {any} asset
*/
function RenderItemInfo( container, description, asset )
{
if( currentAbortController )
{
currentAbortController.abort();
}
currentAbortController = new AbortController();
const abortController = currentAbortController;
const footer = document.createElement( 'steamdb-iteminfo-footer' );
footer.hidden = true;
if( hasBadgeInfoEnabled && window.g_bViewingOwnProfile && description.appid === 753 && description.tags )
{
let itemClass = null;
for( const tag of description.tags )
{
if( tag.category === 'item_class' )
{
itemClass = tag.internal_name;
break;
}
}
if(
itemClass === 'item_class_2' || // trading card
itemClass === 'item_class_5' // booster pack
)
{
const element = document.createElement( 'div' );
element.className = 'steamdb_badge_info';
footer.append( element );
footer.hidden = false;
LoadBadgeInformation( element, description, window.UserYou.GetSteamId() );
}
}
if( hasLinksEnabled && description.appid === 753 && description.owner_actions )
{
let isGift = false;
for( const action of description.owner_actions )
{
const url = new URL( action.link );
if( url.pathname.startsWith( '/checkout/sendgift/' ) || url.pathname.startsWith( 'UnpackGift(' ) || url.pathname.startsWith( 'UnpackGiftItemReward(' ) )
{
isGift = true;
break;
}
}
if( isGift )
{
const element = document.createElement( 'div' );
element.className = 'steamdb_gift_info';
footer.append( element );
footer.hidden = false;
LoadGiftInformation( element, description.classid, asset.assetid, abortController.signal );
}
}
if( hasQuickSellEnabled && description.marketable && !description.is_currency )
{
const element = document.createElement( 'div' );
element.className = 'steamdb_quicksell';
footer.append( element );
footer.hidden = false;
GetMarketItemNameId( description, ( commodityID ) =>
{
if( !commodityID )
{
return;
}
LoadQuickSellInformation( element, commodityID, abortController.signal );
} );
}
requestAnimationFrame( () =>
{
container.append( footer );
} );
};
/**
* @param {HTMLElement} element
* @param {string} commodityID
* @param {AbortSignal} signal
*/
function LoadQuickSellInformation( element, commodityID, signal )
{
const histogramParams = new URLSearchParams();
histogramParams.set( 'country', window.g_rgWalletInfo.wallet_country );
histogramParams.set( 'language', window.g_strLanguage );
histogramParams.set( 'currency', window.g_rgWalletInfo.wallet_currency );
histogramParams.set( 'item_nameid', commodityID );
fetch( '/market/itemordershistogram?' + histogramParams.toString(), {
signal,
headers: {
'X-Requested-With': 'SteamDB',
},
} )
.then( ( response ) =>
{
if( response.status === 429 )
{
// If user is currently rate limited by Steam market, just disable the buttons
hasQuickSellEnabled = false;
}
if( !response.ok )
{
return null;
}
return response.json();
} )
.then( ( data ) =>
{
if( !data || !data.success )
{
return;
}
const hoverText = document.createElement( 'div' );
hoverText.className = 'steamdb_orders_hover_text';
hoverText.textContent = i18n.inventory_quick_sell_tip;
const orderHeaderSummaries = document.createElement( 'div' );
/**
* @param {HTMLElement} button
*/
const BindSellButton = ( button ) =>
{
button.addEventListener( 'click', OnQuickSellButtonClick );
button.addEventListener( 'pointerenter', function()
{
const isSellNow = button.classList.contains( 'steamdb_buy_summary' );
const str = isSellNow ? i18n.inventory_sell_at : i18n.inventory_list_at;
const price = Number.parseInt( button.dataset.price, 10 );
const priceAfterFees = window.GetItemPriceFromTotal( price, window.g_rgWalletInfo );
hoverText.textContent = str.replace( '%price%', FormatCurrency( price ) );
const priceAfterFeesElement = document.createElement( 'span' );
priceAfterFeesElement.textContent = ' → ' + FormatCurrency( priceAfterFees );
hoverText.append( priceAfterFeesElement );
hoverText.classList.add( 'steamdb_hover_visible' );
} );
button.addEventListener( 'pointerleave', function()
{
hoverText.classList.remove( 'steamdb_hover_visible' );
} );
};
if( data.sell_order_summary )
{
const sellHeader = document.createElement( 'div' );
sellHeader.className = 'steamdb_orders_header steamdb_sell_summary';
sellHeader.innerHTML = data.sell_order_summary;
if( data.lowest_sell_order )
{
sellHeader.dataset.price = data.lowest_sell_order.toString();
BindSellButton( sellHeader );
}
orderHeaderSummaries.append( sellHeader );
}
if( data.buy_order_summary )
{
const buyHeader = document.createElement( 'div' );
buyHeader.className = 'steamdb_orders_header steamdb_buy_summary';
buyHeader.innerHTML = data.buy_order_summary;
if( data.highest_buy_order )
{
buyHeader.dataset.price = data.highest_buy_order.toString();
BindSellButton( buyHeader );
}
orderHeaderSummaries.append( buyHeader );
}
const orderHeader = document.createElement( 'div' );
orderHeader.className = 'steamdb_order_header';
orderHeader.append( orderHeaderSummaries );
const logoImage = document.createElement( 'img' );
logoImage.className = 'steamdb_icon';
logoImage.src = logoSrc;
const logoUrl = document.createElement( 'a' );
logoUrl.title = i18n.steamdb_options;
logoUrl.href = optionsUrl;
logoUrl.target = '_blank';
logoUrl.append( logoImage );
orderHeader.append( logoUrl );
element.append( orderHeader );
const hoverTextContainer = document.createElement( 'div' );
hoverTextContainer.append( hoverText );
element.append( hoverTextContainer );
for( const promote of element.querySelectorAll( '.market_commodity_orders_header_promote' ) )
{
promote.className = 'steamdb_orders_header_promote';
}
if( data.sell_order_table )
{
element.insertAdjacentHTML( 'beforeend', data.sell_order_table );
/** @type {HTMLTableElement} */
const table = element.querySelector( '.market_commodity_orders_table' );
table.className = 'steamdb_orders_table';
const rows = table.querySelectorAll( 'tr' );
for( const row of rows )
{
const td = row.querySelector( 'td' );
if( !td )
{
continue;
}
const priceText = td.textContent.trim();
const priceValue = window.GetPriceValueAsInt( priceText );
if( priceValue < 1 )
{
continue;
}
row.classList.add( 'steamdb_order_row_clickable' );
row.dataset.price = priceValue.toString();
BindSellButton( row );
}
if( data.lowest_sell_order )
{
const nFloor = Number.parseInt( window.g_rgWalletInfo.wallet_market_minimum ?? 1, 10 );
const nIncrement = Number.parseInt( window.g_rgWalletInfo.wallet_currency_increment ?? 1, 10 );
const undercutPrice = data.lowest_sell_order - nIncrement;
if( undercutPrice >= ( 3 * nFloor ) && undercutPrice > data.highest_buy_order )
{
const row = document.createElement( 'tr' );
row.classList.add( 'steamdb_order_row_clickable' );
row.style.fontStyle = 'italic';
row.dataset.price = undercutPrice.toString();
const priceCell = document.createElement( 'td' );
priceCell.textContent = FormatCurrency( undercutPrice );
row.append( priceCell );
const quantityCell = document.createElement( 'td' );
row.append( quantityCell );
BindSellButton( row );
rows[ 0 ].after( row );
}
}
}
element.classList.add( 'steamdb_quicksell_visible' );
const actualHeight = element.offsetHeight;
if( actualHeight > quickSellHeight )
{
quickSellHeight = actualHeight;
document.body.style.setProperty( '--steamdb-quick-sell-height', `${actualHeight}px` );
}
} )
.catch( ( e ) =>
{
if( e.name !== 'AbortError' )
{
console.error( '[SteamDB] Quick sell error', e );
}
} );
}
/**
* @param {HTMLElement} element
* @param {string} classid
* @param {string} assetid
* @param {AbortSignal} signal
*/
function LoadGiftInformation( element, classid, assetid, signal )
{
const linkSpan = document.createElement( 'span' );
linkSpan.textContent = i18n.view_on_steamdb;
const link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_small_thin';
link.href = '#';
link.target = '_blank';
link.append( linkSpan );
element.append( link );
const CreateOwnedIcon = () =>
{
const ownedSpan = document.createElement( 'span' );
ownedSpan.textContent = ' ' + i18n.in_library;
const ownedIcon = document.createElement( 'i' );
ownedIcon.className = 'ico16 thumb_upv6';
ownedSpan.prepend( ownedIcon );
const ownedLink = document.createElement( 'a' );
ownedLink.className = 'btnv6_blue_hoverfade btn_small_thin';
ownedLink.append( ownedSpan );
element.append( ownedLink );
};
if( giftCache[ classid ] )
{
link.href = `${homepage}sub/${giftCache[ classid ].packageid}/`;
if( giftCache[ classid ].owned )
{
CreateOwnedIcon();
}
return;
}
link.classList.add( 'btn_disabled' );
// Fetch gift information
fetch( `/gifts/${assetid}/validateunpack`, {
signal,
headers: {
'X-Requested-With': 'SteamDB',
},
} )
.then( ( response ) => response.json() )
.then( ( data ) =>
{
if( data?.packageid )
{
giftCache[ classid ] = { packageid: data.packageid, owned: data.owned || false };
link.classList.remove( 'btn_disabled' );
link.href = `${homepage}sub/${data.packageid}/`;
if( data.owned )
{
CreateOwnedIcon();
}
}
} )
.catch( ( err ) =>
{
if( err.name !== 'AbortError' )
{
console.error( '[SteamDB] Gift info error', err );
}
} );
}
let badgesDataLoaded = false;
/** @type {any[]} */
let badgesData = [];
/**
* @param {HTMLElement} element
* @param {any} description
* @param {string} steamid
*/
function AddBadgeInformation( element, description, steamid )
{
if( !description.market_fee_app )
{
return;
}
let isTradingCard = false;
let isFoilCard = false;
let foundBadge = false;
for( const tag of description.tags )
{
if( tag.category === 'cardborder' )
{
isFoilCard = tag.internal_name !== 'cardborder_0';
}
else if( tag.category === 'item_class' )
{
isTradingCard = tag.internal_name === 'item_class_2';
}
}
/**
* @param {boolean} foil
*/
const CreateLink = ( foil ) =>
`https://steamcommunity.com/profiles/${steamid}/gamecards/${description.market_fee_app}${foil ? '?border=1' : ''}`;
for( const badge of badgesData )
{
if( badge.appid !== description.market_fee_app )
{
continue;
}
const isFoilBadge = badge.border_color > 0;
if( isTradingCard && isFoilCard !== isFoilBadge )
{
continue;
}
foundBadge = true;
const span = document.createElement( 'span' );
const str = isFoilBadge ? i18n.inventory_badge_foil_level : i18n.inventory_badge_level;
span.textContent = str.replace( '%level%', badge.level.toString() );
const link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_small_thin';
link.href = CreateLink( isFoilBadge );
link.append( span );
element.append( link );
}
if( !foundBadge )
{
const span = document.createElement( 'span' );
span.textContent = i18n.inventory_badge_none;
const link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_small_thin';
link.href = CreateLink( isFoilCard );
link.append( span );
element.append( link );
}
}
/**
* @param {HTMLElement} element
* @param {any} description
* @param {string} steamid
*/
function LoadBadgeInformation( element, description, steamid )
{
if( badgesDataLoaded )
{
if( badgesData.length > 0 )
{
AddBadgeInformation( element, description, steamid );
}
return;
}
// TODO: This has a race condition if user switches to another item before the fetch request completes
// but the only problem they will get is no badge info will be displayed.
badgesDataLoaded = true;
const applicationConfigElement = document.getElementById( 'application_config' );
if( !applicationConfigElement )
{
return;
}
const applicationConfig = JSON.parse( applicationConfigElement.dataset.config );
const accessToken = JSON.parse( applicationConfigElement.dataset.loyalty_webapi_token );
if( !accessToken )
{
return;
}
const params = new URLSearchParams();
params.set( 'origin', location.origin );
params.set( 'format', 'json' );
params.set( 'access_token', accessToken );
params.set( 'steamid', steamid );
params.set( 'x_requested_with', 'SteamDB' );
fetch( `${applicationConfig.WEBAPI_BASE_URL}IPlayerService/GetBadges/v1/?${params.toString()}` )
.then( ( response ) => response.json() )
.then( ( response ) =>
{
if( response.response?.badges )
{
badgesData = response.response.badges;
AddBadgeInformation( element, description, steamid );
}
} )
.catch( ( err ) =>
{
console.error( '[SteamDB] Badge info error', err );
} );
}
/**
* @param {any} description
* @param {(commodityID: string|null) => void} callback
*/
function GetMarketItemNameId( description, callback )
{
const appid = description.appid;
const marketHashName = encodeURIComponent( window.GetMarketHashName( description ) );
const cacheKey = `${appid}_${marketHashName}`;
GetCachedItemId( cacheKey )
.then( ( value ) =>
{
if( value )
{
callback( value );
return;
}
fetch( `/market/listings/${appid}/${marketHashName}`, {
headers: {
'X-Requested-With': 'SteamDB',
},
} )
.then( ( response ) =>
{
if( response.status === 429 )
{
// If user is currently rate limited by Steam market, just disable the buttons
hasQuickSellEnabled = false;
}
if( !response.ok )
{
callback( null );
return null;
}
return response.text();
} )
.then( ( data ) =>
{
if( !data )
{
return;
}
const commodityID = data.match( /Market_LoadOrderSpread\(\s?(?<id>\d+)\s?\);/ );
if( !commodityID )
{
callback( null );
return;
}
SetCachedItemId( cacheKey, commodityID.groups.id )
.then( () =>
{
callback( commodityID.groups.id );
} )
.catch( ( err ) =>
{
console.error( '[SteamDB] DB set fail', err );
callback( commodityID.groups.id );
} );
} )
.catch( ( err ) =>
{
console.error( '[SteamDB] Fetch error', err );
callback( null );
} );
} )
.catch( ( err ) =>
{
console.error( '[SteamDB] DB get fail', err );
callback( null );
} );
}
/**
* IndexedDB. Ref: https://github.com/jakearchibald/idb-keyval
*/
const itemDatabase = CreateItemStore( 'steamdb_extension', 'itemid_name_cache' );
/**
* Promisifies an IndexedDB request
* @param {IDBRequest<T>|IDBTransaction} request - The IndexedDB request to promisify
* @returns {Promise<T>} A promise that resolves with the request result
* @template T
*/
function PromisifyDbRequest( request )
{
return new Promise( ( resolve, reject ) =>
{
// @ts-ignore
request.oncomplete = request.onsuccess = () => resolve( request.result );
// @ts-ignore
request.onabort = request.onerror = () => reject( request.error );
} );
}
/**
* Creates an IndexedDB store with the specified name
* @param {string} dbName - The name of the database
* @param {string} storeName - The name of the object store
* @returns {function(IDBTransactionMode, function(IDBObjectStore): T|PromiseLike<T>): Promise<T>} A function that executes callbacks against the store
* @template T
*/
function CreateItemStore( dbName, storeName )
{
const request = indexedDB.open( dbName );
request.onupgradeneeded = () => request.result.createObjectStore( storeName );
const dbp = PromisifyDbRequest( request );
return ( txMode, callback ) => dbp.then( ( db ) => callback( db.transaction( storeName, txMode ).objectStore( storeName ) ) );
}
/**
* Get a value by its key.
* @param {IDBValidKey} key - The key to look up
* @returns {Promise<string|undefined>} A promise that resolves with the stored value or undefined if not found
*/
function GetCachedItemId( key )
{
return itemDatabase( 'readonly', ( store ) => PromisifyDbRequest( store.get( key ) ) );
}
/**
* Set a value with a key.
* @param {IDBValidKey} key - The key to store the value under
* @param {string} value - The value to store
* @returns {Promise<void>} A promise that resolves when the transaction completes
*/
function SetCachedItemId( key, value )
{
return itemDatabase( 'readwrite', ( store ) =>
{
store.put( value, key );
return PromisifyDbRequest( store.transaction );
} );
}
} )() );

View File

@@ -0,0 +1,21 @@
'use strict';
GetOption( { 'enhancement-no-linkfilter': false }, ( items ) =>
{
if( items[ 'enhancement-no-linkfilter' ] )
{
if( window.location && window.location.search )
{
const params = new URLSearchParams( window.location.search );
if( params.has( 'u' ) )
{
window.location.replace( params.get( 'u' ) );
}
else if( params.has( 'url' ) )
{
window.location.replace( params.get( 'url' ) );
}
}
}
} );

View File

@@ -0,0 +1,165 @@
'use strict';
// If prototype.js already loaded, use its event
if( 'observe' in Event )
{
// @ts-ignore
Event.observe( document, 'dom:loaded', OnLoaded );
}
else
{
document.addEventListener( 'DOMContentLoaded', OnLoaded );
}
function OnLoaded()
{
if( window.CAjaxPagingControls )
{
const originalGoToPage = window.CAjaxPagingControls.prototype.GoToPage;
const originalOnAJAXComplete = window.CAjaxPagingControls.prototype.OnAJAXComplete;
const originalOnResponseRenderResults = window.CAjaxPagingControls.prototype.OnResponseRenderResults;
const loader = document.createElement( 'div' );
loader.className = 'steamdb_market_loader';
loader.hidden = true;
const summary = document.getElementById( 'searchResultsTable' );
if( summary )
{
summary.append( loader );
}
/**
* @param {any} transport
*/
window.CAjaxPagingControls.prototype.OnResponseRenderResults = function SteamDB_OnResponseRenderResults( transport )
{
const response = transport.responseJSON;
if( !response )
{
// Call original but it does nothing for no success
originalOnResponseRenderResults.apply( this, arguments );
return;
}
const responseStart = response.start;
let fixedBug = false;
if( response.success && responseStart > 0 && response.total_count < 1 )
{
fixedBug = true;
response.start = 0;
console.log( '[SteamDB] Steam returned 0 results, but user was trying to load a page, fixing this' );
}
originalOnResponseRenderResults.apply( this, arguments );
if( fixedBug )
{
// If user tries to fetch some page, but Steam says there are no results and returns an error html,
// it normally screws the state of the pagination
this.m_iCurrentPage = Math.floor( responseStart / this.m_cPageSize );
this.m_cMaxPages = this.m_iCurrentPage + 1;
}
};
/**
* @param {number} iPage
*/
window.CAjaxPagingControls.prototype.GoToPage = function SteamDB_GoToPage( iPage )
{
if( this.m_strElementPrefix !== 'searchResults' )
{
originalGoToPage.apply( this, arguments );
return;
}
// If initial page load has no count, but somehow is trying to go to a page,
// force the page check to pass otherwise it will not try to load anything
if( window.g_oSearchData && window.g_oSearchData.total_count < 1 && this.m_cMaxPages < 1 )
{
this.m_cMaxPages = iPage + 1;
console.log( '[SteamDB] Page loaded with 0 results, fixing this' );
}
originalGoToPage.apply( this, arguments );
if( this.m_bLoading )
{
loader.hidden = false;
}
};
/**
* @param {any} transport
*/
window.CAjaxPagingControls.prototype.OnAJAXComplete = function( transport )
{
originalOnAJAXComplete.apply( this, arguments );
if( this.m_strElementPrefix !== 'searchResults' )
{
return;
}
loader.hidden = true;
AddRetryMarketButton( this );
// If the request fail, cache bust future requests, otherwise retrying will just hit browser cache
if( !transport.responseJSON || !transport.responseJSON.success || transport.responseJSON.total_count < 1 )
{
if( this.m_rgStaticParams === null )
{
this.m_rgStaticParams = {};
}
this.m_rgStaticParams.steamdb_cache = Date.now().toString();
}
};
}
/**
* @param {any} context
*/
function AddRetryMarketButton( context )
{
const message = document.querySelector( '#searchResultsTable .market_listing_table_message' );
if( !message )
{
return;
}
const div = document.createElement( 'div' );
div.className = 'steamdb_market_retry_button';
const btn = document.createElement( 'button' );
btn.className = 'btnv6_green_white_innerfade btn_medium';
const span = document.createElement( 'span' );
span.textContent = 'Try again';
btn.append( span );
btn.addEventListener( 'click', () =>
{
btn.remove();
context.GoToPage( context.m_iCurrentPage, true );
} );
div.append( btn );
message.append( div );
}
setTimeout( () =>
{
if( window.g_oSearchResults )
{
AddRetryMarketButton( window.g_oSearchResults );
}
}, 100 );
}

View File

@@ -0,0 +1,29 @@
'use strict';
GetOption( { 'enhancement-market-ssa': false }, ( items ) =>
{
if( items[ 'enhancement-market-ssa' ] )
{
/** @type {HTMLInputElement} */
let element = document.querySelector( '#market_buynow_dialog_accept_ssa' );
if( element )
{
element.checked = true;
}
element = document.querySelector( '#market_buyorder_dialog_accept_ssa' );
if( element )
{
element.checked = true;
}
element = document.querySelector( '#market_sell_dialog_accept_ssa' );
if( element )
{
element.checked = true;
}
}
} );

View File

@@ -0,0 +1,53 @@
'use strict';
( ( () =>
{
const originalOrderPollingComplete = window.OrderPollingComplete;
window.OrderPollingComplete = function SteamDB_OrderPollingComplete()
{
originalOrderPollingComplete.apply( this, arguments );
// Verify that all purchases succeeded
for( let iOrder = 0; iOrder < window.g_rgItemNameIds.length; iOrder++ )
{
const order = window.g_rgOrders[ iOrder ];
if( order.m_nQuantity < 1 )
{
continue;
}
if( !order.m_bOrderSuccess )
{
return;
}
const success = document.getElementById( `buy_${order.m_llNameId}_success` );
// If the success checkmark is not visible, something went wrong
if( !success || !success.checkVisibility() )
{
return;
}
}
const params = new URLSearchParams( window.location.search );
const returnTo = params.get( 'steamdb_return_to' );
if( returnTo === null )
{
return;
}
const returnToUrl = new URL( returnTo );
// Verify that we're returning to the same origin
if( returnToUrl.origin !== window.location.origin )
{
return;
}
window.location.href = returnToUrl.toString();
};
} )() );

View File

@@ -0,0 +1,93 @@
'use strict';
GetOption( {
'profile-calculator': true,
'enhancement-award-popup-url': true,
}, ( items ) =>
{
if( items[ 'enhancement-award-popup-url' ] && window.location.search.includes( 'award' ) )
{
const script = document.createElement( 'script' );
script.id = 'steamdb_profile_award';
script.type = 'text/javascript';
script.src = GetLocalResource( 'scripts/community/profile_award_injected.js' );
document.head.appendChild( script );
}
if( !items[ 'profile-calculator' ] )
{
return;
}
// Can't access g_rgProfileData inside sandbox :(
let steamID = '';
let isCommunityID = false;
// If we can, use abuseID
/** @type {HTMLInputElement} */
const abuseIDInput = document.querySelector( '#abuseForm > input[name=abuseID]' );
if( abuseIDInput )
{
steamID = abuseIDInput.value;
isCommunityID = true;
}
else
{
// Fallback to url if we can't
steamID = location.pathname.match( /^\/(?:id|profiles)\/([^\s/]+)\/?/ )[ 1 ];
isCommunityID = /^\/profiles/.test( location.pathname );
}
let container = document.querySelector( '#profile_action_dropdown .popup_body' );
let url = GetHomepage() + 'calculator/';
if( isCommunityID )
{
url += `${steamID}/`;
}
else
{
url += `?player=${steamID}`;
}
if( container )
{
const image = document.createElement( 'img' );
image.className = 'steamdb_popup_icon';
image.src = GetLocalResource( 'icons/white.svg' );
const element = document.createElement( 'a' );
element.href = url;
element.className = 'popup_menu_item';
element.appendChild( image );
element.appendChild( document.createTextNode( '\u00a0 ' + _t( 'steamdb_calculator' ) ) );
container.insertBefore( element, null );
}
else
{
container = document.querySelector( '.profile_header_actions' );
if( container )
{
const image = document.createElement( 'img' );
image.src = GetLocalResource( 'icons/white.svg' );
image.className = 'steamdb_self_profile';
const text = document.createElement( 'span' );
text.dataset.tooltipText = _t( 'steamdb_calculator' );
text.appendChild( image );
const element = document.createElement( 'a' );
element.className = 'btn_profile_action btn_medium';
element.href = url;
element.appendChild( text );
container.appendChild( element );
}
}
} );

View File

@@ -0,0 +1,28 @@
'use strict';
( ( () =>
{
if( !( 'g_rgProfileData' in window ) || !( 'fnLoyalty_ShowAwardModal' in window ) )
{
return;
}
const params = new URLSearchParams( window.location.search );
const awardId = params.get( 'award' );
if( awardId === null )
{
return;
}
window.fnLoyalty_ShowAwardModal(
window.g_rgProfileData.steamid,
3, // profile
() =>
{
// do nothing
},
undefined, // ugcType
Number.parseInt( awardId, 10 ),
);
} )() );

View File

@@ -0,0 +1,93 @@
'use strict';
const progressInfo = document.querySelectorAll( '.badge_title_stats_drops .progress_info_bold' );
if( progressInfo.length > 0 )
{
let apps = 0;
let drops = 0;
let match;
for( let i = 0; i < progressInfo.length; i++ )
{
match = progressInfo[ i ].textContent.match( /(?<number>[0-9]+)/ );
if( match )
{
match = Number.parseInt( match.groups.number, 10 ) || 0;
if( match > 0 )
{
apps++;
drops += match;
}
}
}
if( apps > 0 )
{
const container = document.querySelector( '.badge_details_set_favorite' );
if( container )
{
const hasPages = document.querySelector( '.pageLinks' );
let text = document.createElement( 'span' );
text.className = 'steamdb_drops_remaining';
text.appendChild( document.createTextNode( _t( hasPages ? 'badges_idle_apps_on_this_page' : 'badges_idle_apps', [ apps.toString() ] ) ) );
container.prepend( text );
container.prepend( document.createTextNode( ' ' ) );
text = document.createElement( 'span' );
text.className = 'steamdb_drops_remaining';
text.appendChild( document.createTextNode( _t( hasPages ? 'badges_idle_drops_on_this_page' : 'badges_idle_drops', [ drops.toString() ] ) ) );
container.prepend( text );
}
}
}
else
{
GetOption( { 'button-gamecards': true }, ( items ) =>
{
if( !items[ 'button-gamecards' ] )
{
return;
}
const profileTexture = document.querySelector( '.profile_small_header_texture' );
if( !profileTexture )
{
return;
}
const badgeUrl = location.pathname.match( /\/badges\/([0-9]+)/ );
if( !badgeUrl )
{
return;
}
const badgeid = Number.parseInt( badgeUrl[ 1 ], 10 );
const container = document.createElement( 'div' );
container.className = 'profile_small_header_additional steamdb';
const image = document.createElement( 'img' );
image.className = 'ico16';
image.src = GetLocalResource( 'icons/white.svg' );
const span = document.createElement( 'span' );
span.dataset.tooltipText = _t( 'view_on_steamdb' );
span.appendChild( image );
const link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_medium btn_steamdb';
link.href = GetHomepage() + 'badge/' + badgeid + '/';
link.appendChild( span );
container.insertBefore( link, container.firstChild );
profileTexture.appendChild( container );
} );
}

View File

@@ -0,0 +1,93 @@
'use strict';
MoveMultiBuyButton();
GetOption( { 'button-gamecards': true }, ( items ) =>
{
if( !items[ 'button-gamecards' ] )
{
return;
}
const profileTexture = document.querySelector( '.profile_small_header_texture' );
if( !profileTexture )
{
return;
}
// Container
const container = document.createElement( 'div' );
container.className = 'profile_small_header_additional steamdb';
// Store button
let span = document.createElement( 'span' );
span.appendChild( document.createTextNode( _t( 'store_page' ) ) );
let link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_medium';
link.href = 'https://store.steampowered.com/app/' + GetCurrentAppID() + '/';
link.appendChild( span );
container.insertBefore( link, container.firstChild );
// SteamDB button
const image = document.createElement( 'img' );
image.className = 'ico16';
image.src = GetLocalResource( 'icons/white.svg' );
span = document.createElement( 'span' );
span.dataset.tooltipText = _t( 'view_on_steamdb' );
span.appendChild( image );
link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_medium btn_steamdb';
link.href = GetHomepage() + 'app/' + GetCurrentAppID() + '/communityitems/';
link.appendChild( span );
container.insertBefore( link, container.firstChild );
container.insertBefore( document.createTextNode( ' ' ), link.nextSibling );
// Add to the page
profileTexture.appendChild( container );
} );
function MoveMultiBuyButton()
{
/** @type {NodeListOf<HTMLAnchorElement>} */
const links = document.querySelectorAll( '.gamecards_inventorylink a' );
for( const element of links )
{
const link = new URL( element.href );
// Fix Valve incorrectly using CDN in the link
if( link.host.endsWith( '.steamstatic.com' ) )
{
link.host = window.location.host;
element.href = link.toString();
}
if( link.pathname === '/market/multibuy' )
{
// Add return to link to automatically return to the badge page after multi buying the cards
const params = new URLSearchParams( link.search );
params.set( 'steamdb_return_to', window.location.href );
link.search = params.toString();
element.href = link.toString();
// Move the buy button up top
const topLinks = document.querySelector( '.badge_detail_tasks .gamecards_inventorylink' );
if( topLinks )
{
topLinks.append( element );
topLinks.append( document.createTextNode( ' ' ) );
// Some languages will overflow the buttons so we have to correct the spacing
topLinks.classList.add( 'steamdb_gamecards_inventorylink' );
}
}
}
}

View File

@@ -0,0 +1,49 @@
'use strict';
if( document.getElementById( 'inventory_link_753' ) )
{
GetOption( {
'link-inventory': true,
'enhancement-inventory-sidebar': true,
'enhancement-inventory-quick-sell': true,
'enhancement-inventory-quick-sell-auto': false,
'enhancement-inventory-no-sell-reload': true,
'enhancement-inventory-badge-info': true,
}, ( items ) =>
{
if( items[ 'enhancement-inventory-sidebar' ] )
{
const style = document.createElement( 'link' );
style.id = 'steamdb_inventory_sidebar';
style.type = 'text/css';
style.rel = 'stylesheet';
style.href = GetLocalResource( 'styles/inventory-sidebar.css' );
document.head.appendChild( style );
}
const element = document.createElement( 'script' );
element.id = 'steamdb_inventory_hook';
element.type = 'text/javascript';
element.src = GetLocalResource( 'scripts/community/inventory.js' );
element.dataset.homepage = GetHomepage();
element.dataset.logo = GetLocalResource( 'icons/white.svg' );
element.dataset.optionsUrl = GetLocalResource( 'options/options.html' ) + '#inventory';
element.dataset.options = JSON.stringify( items );
element.dataset.i18n = JSON.stringify( {
steamdb_options: _t( 'steamdb_options' ),
view_on_steamdb: _t( 'view_on_steamdb' ),
in_library: _t( 'in_library' ),
inventory_quick_sell_tip: _t( 'inventory_quick_sell_tip' ),
inventory_list_at: _t( 'inventory_list_at' ),
inventory_sell_at: _t( 'inventory_sell_at' ),
inventory_list_at_title: _t( 'inventory_list_at_title' ),
inventory_sell_at_title: _t( 'inventory_sell_at_title' ),
inventory_badge_level: _t( 'inventory_badge_level' ),
inventory_badge_foil_level: _t( 'inventory_badge_foil_level' ),
inventory_badge_none: _t( 'inventory_badge_none' ),
} );
document.head.appendChild( element );
} );
}

View File

@@ -0,0 +1,30 @@
'use strict';
GetOption( { 'button-gamehub': true }, ( items ) =>
{
if( !items[ 'button-gamehub' ] )
{
return;
}
const container = document.querySelector( '.review_app_actions' );
if( !container )
{
return;
}
// image
const image = document.createElement( 'img' );
image.className = 'toolsIcon steamdb_ogg_icon';
image.src = GetLocalResource( 'icons/white.svg' );
// link
const link = document.createElement( 'a' );
link.className = 'general_btn panel_btn';
link.href = GetHomepage() + 'app/' + GetCurrentAppID() + '/';
link.appendChild( image );
link.appendChild( document.createTextNode( _t( 'view_on_steamdb' ) ) );
container.insertBefore( link, null );
} );

View File

@@ -0,0 +1,25 @@
'use strict';
GetOption( {
'enhancement-tradeoffer-url-items': true,
'enhancement-tradeoffer-no-gift-confirm': null,
}, ( items ) =>
{
const element = document.createElement( 'script' );
if( items[ 'enhancement-tradeoffer-no-gift-confirm' ] )
{
element.dataset.noGiftConfirm = 'true';
}
if( items[ 'enhancement-tradeoffer-url-items' ] )
{
element.dataset.urlItemSupport = 'true';
}
element.id = 'steamdb_tradeoffer';
element.type = 'text/javascript';
element.src = GetLocalResource( 'scripts/community/tradeoffer_injected.js' );
document.head.appendChild( element );
} );

View File

@@ -0,0 +1,174 @@
'use strict';
( ( () =>
{
if( !window.CTradeOfferStateManager )
{
return;
}
const script = document.getElementById( 'steamdb_tradeoffer' );
// for_item support
if( script.dataset.urlItemSupport === 'true' && window.g_rgCurrentTradeStatus && window.location.pathname.startsWith( '/tradeoffer/new' ) )
{
const params = new URLSearchParams( window.location.search );
const theirItems = params.getAll( 'for_item' );
const myItems = params.getAll( 'my_item' );
let redrawTrade = false;
if( theirItems.length > 0 )
{
window.g_rgCurrentTradeStatus.them.ready = false;
window.g_rgCurrentTradeStatus.them.assets = [];
for( const item of theirItems )
{
const parsed = item.match( /(?<appid>[0-9]+)_(?<contextid>[0-9]+)_(?<assetid>[0-9]+)/ );
if( parsed === null )
{
continue;
}
window.g_rgCurrentTradeStatus.them.assets.push( {
appid: parsed.groups.appid,
contextid: parsed.groups.contextid,
assetid: parsed.groups.assetid,
amount: 1,
} );
redrawTrade = true;
}
}
if( myItems.length > 0 )
{
window.g_rgCurrentTradeStatus.me.ready = false;
window.g_rgCurrentTradeStatus.me.assets = [];
for( const item of myItems )
{
const parsed = item.match( /(?<appid>[0-9]+)_(?<contextid>[0-9]+)_(?<assetid>[0-9]+)/ );
if( parsed === null )
{
continue;
}
window.g_rgCurrentTradeStatus.me.assets.push( {
appid: parsed.groups.appid,
contextid: parsed.groups.contextid,
assetid: parsed.groups.assetid,
amount: 1,
} );
redrawTrade = true;
}
}
if( redrawTrade )
{
window.RedrawCurrentTradeStatus();
}
}
// no gift confirmation
if( script.dataset.noGiftConfirm === 'true' )
{
const originalToggleReady = window.ToggleReady;
/**
* @param {any} ready
*/
window.ToggleReady = function( ready )
{
window.g_rgCurrentTradeStatus.me.ready = ready;
window.g_cTheirItemsInTrade = 1;
window.g_bWarnOnReady = false;
originalToggleReady.apply( this, arguments );
};
}
// better error messages
const originalShowAlertDialog = window.ShowAlertDialog;
const originalSetAssetOrCurrencyInTrade = window.CTradeOfferStateManager.SetAssetOrCurrencyInTrade;
/**
* @param {Record<string, any>} item
*/
window.CTradeOfferStateManager.SetAssetOrCurrencyInTrade = function SteamDB_SetAssetOrCurrencyInTrade( item )
{
try
{
// Make sure this item can actually be traded
const appName = window.g_rgPartnerAppContextData[ item.appid ].name;
const errorTitle = 'Cannot Add "' + item.name + '" to Trade';
switch( window.g_rgPartnerAppContextData[ item.appid ].trade_permissions )
{
case 'NONE':
originalShowAlertDialog( errorTitle, window.g_strTradePartnerPersonaName + ' cannot trade items in ' + appName + '.' );
return;
case 'SENDONLY':
case 'SENDONLY_FULLINVENTORY':
if( !item.is_their_item )
{
originalShowAlertDialog( errorTitle, window.g_strTradePartnerPersonaName + ' cannot receive items in ' + appName + ( window.g_rgPartnerAppContextData[ item.appid ].trade_permissions === 'SENDONLY_FULLINVENTORY' ? ' because their inventory is full' : '' ) + '.' );
return;
}
break;
case 'RECEIVEONLY':
if( item.is_their_item )
{
originalShowAlertDialog( errorTitle, window.g_strTradePartnerPersonaName + ' cannot send items in ' + appName + '.' );
return;
}
break;
}
}
catch( ex )
{
// don't care!
}
originalSetAssetOrCurrencyInTrade.apply( this, arguments );
};
/**
* @param {string} strTitle
* @param {string} strDescription
*/
window.ShowAlertDialog = function SteamDB_ShowAlertDialog( strTitle, strDescription )
{
const eresult = strDescription.match( /\(([0-9]+)\)$/ );
if( eresult !== null )
{
let explanation;
switch( +eresult[ 1 ] )
{
case 2: explanation = 'There was an internal error when sending your trade offer.'; break;
case 11: explanation = 'This trade offer is not currently active. It may have been previously accepted or canceled.'; break;
case 16: explanation = 'The Steam Community servers did not get a timely reply from the economy server. Your offer may or may not have been sent.<br><br>Please check your sent trade offers.'; break;
case 20: explanation = 'The trade offer server is temporarily unavailable.'; break;
case 25: explanation = 'You cannot send this trade offer because you have exceeded your active offer limit.<br><br>You are limited to 5 outstanding trade offers to a single user, and 30 outstanding trade offers in total.<br><br>If you are accepting a trade offer, then your inventory for a particular game may be full.'; break;
case 26: explanation = 'One or more of the items in this trade offer is no longer present in the inventory from which it is being requested.<br><br>Please check all items to ensure that they still exist and are tradable.'; break;
}
if( explanation )
{
arguments[ 0 ] += ' Failed';
arguments[ 1 ] += '<p class="steamdb_trade_error">' + explanation + '</p>';
arguments[ 1 ] += '<a href="https://steamdb.info/extension/" target="_blank" class="steamdb_trade_error_explained">(explained by SteamDB)</a>';
}
}
return originalShowAlertDialog.apply( this, arguments );
};
} )() );

View File

@@ -0,0 +1,81 @@
'use strict';
// There's no easier way to check if we're on error page :(
if( document.title === 'Sorry!' ||
document.title === 'Error' ||
document.title === '502 Bad Gateway' ||
document.title === 'We Broke It' )
{
const link = document.createElement( 'a' );
link.href = 'https://steamstat.us';
link.appendChild( document.createTextNode( _t( 'steamstatus' ) ) );
const container = document.createElement( 'div' );
container.className = 'steamdb_downtime';
container.appendChild( document.createTextNode( _t( 'steamstatus_downtime' ) ) );
container.appendChild( link );
document.body.insertBefore( container, document.body.firstChild );
document.body.style.margin = '0';
}
else
{
GetOption( { 'enhancement-hide-install-button': true, 'enhancement-no-linkfilter': false }, ( items ) =>
{
if( items[ 'enhancement-hide-install-button' ] )
{
/** @type {HTMLElement} */
const button = document.querySelector( '.header_installsteam_btn' );
if( button )
{
button.setAttribute( 'hidden', 'true' );
button.style.display = 'none';
}
}
if( items[ 'enhancement-no-linkfilter' ] )
{
/** @type {NodeListOf<HTMLAnchorElement>} */
const links = document.querySelectorAll( 'a[href^="https://steamcommunity.com/linkfilter/"]' );
for( const link of links )
{
if( !link.search )
{
continue;
}
const params = new URLSearchParams( link.search );
if( params.has( 'u' ) )
{
link.href = params.get( 'u' );
}
else if( params.has( 'url' ) )
{
link.href = params.get( 'url' );
}
}
}
} );
const popup = document.querySelector( '#account_dropdown .popup_body' );
if( popup )
{
const optionsLink = document.createElement( 'a' );
optionsLink.target = '_blank';
optionsLink.className = 'popup_menu_item steamdb_options_link';
optionsLink.textContent = ' ' + _t( 'steamdb_options' );
optionsLink.href = GetLocalResource( 'options/options.html' );
const image = document.createElement( 'img' );
image.className = 'ico16';
image.src = GetLocalResource( 'icons/white.svg' );
optionsLink.prepend( image );
popup.appendChild( optionsLink );
}
}

View File

@@ -0,0 +1,202 @@
'use strict';
const EXTENSION_INTEROP_VERSION = 2;
const OnPageLoadedInit = () =>
{
window.postMessage( {
version: EXTENSION_INTEROP_VERSION,
type: 'steamdb:extension-init',
data: {
options_url: GetLocalResource( 'options/options.html' ),
},
}, GetHomepage() );
};
if( document.readyState === 'loading' )
{
document.addEventListener( 'readystatechange', OnPageLoadedInit, { once: true } );
}
else
{
OnPageLoadedInit();
}
window.addEventListener( 'message', ( request ) =>
{
if( !request || !request.data || request.origin !== window.location.origin )
{
return;
}
switch( request.data.type )
{
case 'steamdb:extension-query':
{
if( request.data.contentScriptQuery )
{
SendMessageToBackgroundScript( request.data, ( response ) =>
{
window.postMessage( {
version: EXTENSION_INTEROP_VERSION,
type: 'steamdb:extension-response',
request: request.data,
response,
}, GetHomepage() );
} );
}
break;
}
case 'steamdb:extension-invalidate-cache':
{
WriteLog( 'Invalidating userdata cache' );
SendMessageToBackgroundScript( {
contentScriptQuery: 'InvalidateCache',
}, () =>
{
// noop
} );
break;
}
}
} );
GetOption( { 'steamdb-highlight': true, 'steamdb-highlight-family': true }, async( items ) =>
{
if( !items[ 'steamdb-highlight' ] )
{
return;
}
/** @type {Promise<{data?: Record<string, any>, error?: string}>} */
const userDataPromise = new Promise( ( resolve ) =>
{
SendMessageToBackgroundScript( {
contentScriptQuery: 'FetchSteamUserData',
}, resolve );
} );
/** @type {Promise<{data?: Record<string, any>, error?: string}>} */
const familyDataPromise = new Promise( ( resolve ) =>
{
if( !items[ 'steamdb-highlight-family' ] )
{
resolve( {} );
return;
}
SendMessageToBackgroundScript( {
contentScriptQuery: 'FetchSteamUserFamilyData',
}, resolve );
} );
/** @type {Promise<{data?: undefined, error?: string}>} */
const familyDataTimeoutPromise = new Promise( ( resolve ) =>
{
setTimeout( () =>
{
resolve( { error: 'Family data timed out' } );
}, 10000 ); // 10 seconds
} );
const userData = await userDataPromise;
// If family data does not load fast enough, assume it failed
const familyData = await Promise.race( [
familyDataPromise,
familyDataTimeoutPromise,
] );
if( userData.error )
{
WriteLog( 'Failed to load userdata', userData.error );
window.postMessage( {
version: EXTENSION_INTEROP_VERSION,
type: 'steamdb:extension-error',
error: `Failed to load your games. ${userData.error}`,
}, GetHomepage() );
}
if( familyData.error )
{
WriteLog( 'Failed to load family userdata', familyData.error );
}
/** @type {Record<string, any>} */
let response = null;
if( userData.data )
{
response = userData.data;
if( familyData.data )
{
response.rgFamilySharedApps = familyData.data.rgFamilySharedApps;
if( familyData.data.rgOwnedApps )
{
// Merge owned apps from the shared library because it returns extra apps
// that are not returned by the dynamicstore such as tools
response.rgOwnedApps = Array.from( new Set( [
...response.rgOwnedApps,
...familyData.data.rgOwnedApps
] ) );
}
}
}
const OnPageLoaded = () =>
{
if( response )
{
window.postMessage( {
version: EXTENSION_INTEROP_VERSION,
type: 'steamdb:extension-loaded',
data: response,
}, GetHomepage() );
WriteLog(
'Userdata loaded',
'Packages',
response.rgOwnedPackages?.length || 0,
'Family Apps',
response.rgFamilySharedApps?.length || 0,
);
}
};
const IsSiteReady = () => document.readyState === 'complete' || !!document.getElementById( 'main' );
if( IsSiteReady() )
{
OnPageLoaded();
return;
}
// As we wait for promises to complete first, chances are very high that the main element should be ready by now,
// but to avoid any possible issues we still have a fallback to the mutation observer.
//
// The website has code to process the extension messages loaded in a script before the #main element,
// and this script is not deferred. But to apply the highlights to all the elements correctly,
// the site will have to wait for the DOM to complete loading before applying the highlights.
//
// We avoid waiting for DOMContentLoaded event and instead wait for the receiving script to be ready,
// because postMessage() itself may take time.
WriteLog( 'Data loaded too fast, site is not yet ready.' );
const observer = new MutationObserver( () =>
{
if( IsSiteReady() )
{
observer.disconnect();
OnPageLoaded();
}
} );
observer.observe( document.documentElement, {
childList: true,
subtree: true
} );
} );

View File

@@ -0,0 +1,87 @@
'use strict';
setTimeout( () =>
{
GetOption( { 'link-accountpage': true }, ( items ) =>
{
const addLinks = items[ 'link-accountpage' ];
if( document.readyState === 'loading' )
{
document.addEventListener( 'DOMContentLoaded', OnContentLoaded );
}
else
{
OnContentLoaded();
}
function OnContentLoaded()
{
const table = document.querySelector( '.account_table' );
if( !addLinks || !table )
{
document.body.classList.add( 'steamdb_account_table_loaded' );
return;
}
const licenses = table.querySelectorAll( 'tr' );
if( licenses )
{
const params = new URLSearchParams();
params.set( 'a', 'sub' );
for( const tr of licenses )
{
const nameCell = tr.cells[ 1 ];
if( nameCell.tagName === 'TH' )
{
const newTd = document.createElement( 'th' );
newTd.className = 'steamdb_license_id_col';
newTd.textContent = 'SteamDB';
nameCell.after( newTd );
continue;
}
/** @type {HTMLAnchorElement} */
const link = document.createElement( 'a' );
/** @type {HTMLAnchorElement} */
const removeElement = nameCell.querySelector( '.free_license_remove_link a' );
if( removeElement )
{
const subidMatch = removeElement.href.match( /RemoveFreeLicense\( ?(?<subid>[0-9]+)/ );
if( !subidMatch )
{
continue;
}
const subid = subidMatch.groups.subid;
link.href = `${GetHomepage()}sub/${subid}/`;
link.textContent = subid;
}
else
{
params.set( 'q', nameCell.textContent.trim() );
link.href = `${GetHomepage()}search/?${params.toString()}`;
link.textContent = _t( 'Search' );
}
const newTd = document.createElement( 'td' );
newTd.className = 'steamdb_license_id_col';
newTd.append( link );
nameCell.after( newTd );
}
}
document.body.classList.add( 'steamdb_account_table_loaded' );
}
} );
}, 0 );

View File

@@ -0,0 +1,49 @@
'use strict';
( ( () =>
{
if( document.body )
{
PerformHook();
}
else
{
// If the script was injected too early, wait for <body> element to be created
const observer = new MutationObserver( () =>
{
if( document.body )
{
PerformHook();
observer.disconnect();
}
} );
observer.observe( document, {
childList: true,
subtree: true,
} );
}
function PerformHook()
{
const noop = () =>
{
// noop
};
window.InstrumentLinks = noop;
window.BindTooltips = noop;
if( window.GDynamicStore )
{
window.GDynamicStore.Init = noop;
}
// As Valve's own comment says this function is for "perf sensitive pages"
if( window.DisableTooltipMutationObserver )
{
window.DisableTooltipMutationObserver();
}
}
} )() );

View File

@@ -0,0 +1,67 @@
/* global AddLinksInErrorBox */
'use strict';
if( GetCurrentAppID() > 0 )
{
const selectorsToTry =
[
'#error_box',
'#app_agegate',
'#agegate_box',
'.agegate_age_validation',
];
for( const selector of selectorsToTry )
{
const container = document.querySelector( selector );
if( container )
{
AddLinksInErrorBox( container );
break;
}
}
}
GetOption( { 'enhancement-skip-agecheck': false }, ( items ) =>
{
if( items[ 'enhancement-skip-agecheck' ] )
{
const dateFuture = new Date();
dateFuture.setFullYear( dateFuture.getFullYear() + 1 );
const date = dateFuture.toUTCString();
document.cookie = 'wants_mature_content=1; expires=' + date + '; path=/app/; Secure; SameSite=Lax;';
document.cookie = 'wants_mature_content=1; expires=' + date + '; path=/bundle/; Secure; SameSite=Lax;';
document.cookie = 'lastagecheckage=1-January-1970; expires=' + date + '; path=/; Secure; SameSite=Lax;';
document.cookie = 'birthtime=1; expires=' + date + '; path=/; Secure; SameSite=Lax;';
// Make sure we know how to bypass this agegate before redirecting
// App 526520 causes inifite redirects due to an error message on agecheck url
if( document.querySelector( '#agecheck_form, #app_agegate' ) )
{
document.location.href = document.location.href.replace( /\/agecheck/, '' );
}
}
else
{
const container = document.getElementById( 'app_agegate' );
if( !container )
{
return;
}
const optionsLink = document.createElement( 'a' );
optionsLink.target = '_blank';
optionsLink.textContent = _t( 'agecheck_option_hint' );
optionsLink.href = GetLocalResource( 'options/options.html' ) + '#skip_agecheck';
const linkContainer = document.createElement( 'div' );
linkContainer.className = 'steamdb_error_link steamdb_agecheck_hint';
linkContainer.append( optionsLink );
container.append( linkContainer );
}
} );

View File

@@ -0,0 +1,11 @@
'use strict';
( ( () =>
{
document.querySelectorAll( '.steamdb_dev_pub_link_container .more_btn' ).forEach( ( button ) =>
{
button.remove();
} );
window.CollapseLongStrings( '.steamdb_dev_pub_link_container .summary.column' );
} )() );

View File

@@ -0,0 +1 @@
declare function AddLinksInErrorBox(container: Element): void;

View File

@@ -0,0 +1,50 @@
/* exported AddLinksInErrorBox */
'use strict';
/**
* @param {Element} container
*/
function AddLinksInErrorBox( container )
{
const pageTypeMatch = location.pathname.match( /^\/(?:[a-z]+\/)?(?<type>app|bundle|sub)\// );
if( !pageTypeMatch )
{
return;
}
const pageType = pageTypeMatch.groups.type;
const linkContainer = document.createElement( 'div' );
linkContainer.className = 'steamdb_error_link';
// SteamDB
let link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_medium';
link.href = `${GetHomepage()}${pageType}/${GetCurrentAppID()}/`;
let text = document.createElement( 'span' );
text.append( document.createTextNode( _t( 'view_on_steamdb' ) ) );
link.append( text );
linkContainer.append( link );
// PCGW
if( pageType === 'app' )
{
linkContainer.append( document.createTextNode( ' ' ) );
link = document.createElement( 'a' );
link.className = 'btnv6_blue_hoverfade btn_medium';
link.href = 'https://pcgamingwiki.com/api/appid.php?appid=' + GetCurrentAppID() + '&utm_source=SteamDB';
text = document.createElement( 'span' );
text.append( document.createTextNode( _t( 'view_on_pcgamingwiki' ) ) );
link.append( text );
linkContainer.append( link );
}
container.append( linkContainer );
}

View File

@@ -0,0 +1,53 @@
'use strict';
GetOption( {
'prevent-store-images': false,
}, ( items ) =>
{
if( !items[ 'prevent-store-images' ] )
{
return;
}
/** @type {HTMLImageElement[]} */
const images = Array.from( document.querySelectorAll( '.game_area_description img, .early_access_announcements img' ) );
if( !images.length )
{
return;
}
for( const image of images )
{
if( image.complete )
{
continue;
}
image.style.width = ( image.width || 100 ) + 'px';
image.style.height = ( image.height || 100 ) + 'px';
image.dataset.src = image.src;
image.src = GetLocalResource( 'icons/image.svg' );
image.addEventListener( 'click', ImageClick, { once: true } );
}
/**
* @param {MouseEvent} e
* @this {HTMLImageElement}
*/
function ImageClick( e )
{
this.addEventListener( 'load', ImageLoad, { once: true } );
this.src = this.dataset.src;
e.preventDefault();
}
/**
* @this {HTMLImageElement}
*/
function ImageLoad()
{
this.style.width = '';
this.style.height = '';
}
} );

View File

@@ -0,0 +1,10 @@
/* global AddLinksInErrorBox */
'use strict';
const container = document.getElementById( 'error_box' );
if( container && GetCurrentAppID() > 0 )
{
AddLinksInErrorBox( container );
}

View File

@@ -0,0 +1,28 @@
'use strict';
GetOption( { 'button-sub': true }, ( items ) =>
{
if( !items[ 'button-sub' ] )
{
return;
}
const container = document.querySelector( '.game_meta_data' );
if( container )
{
let element = document.createElement( 'span' );
element.appendChild( document.createTextNode( _t( 'view_on_steamdb' ) ) );
const link = document.createElement( 'a' );
link.className = 'action_btn';
link.href = GetHomepage() + 'bundle/' + GetCurrentAppID() + '/';
link.appendChild( element );
element = document.createElement( 'div' );
element.className = 'block';
element.appendChild( link );
container.insertBefore( element, container.firstChild );
}
} );

View File

@@ -0,0 +1,242 @@
'use strict';
// Fix Valve's bug where empty queue banner has wrong height and it hides text
/** @type {HTMLElement} */
const emptyQueue = document.querySelector( '.discover_queue_empty' );
if( emptyQueue )
{
emptyQueue.style.height = 'auto';
}
const applicationConfigElement = document.getElementById( 'application_config' );
if( !applicationConfigElement )
{
throw new Error( 'Failed to find application_config' );
}
const storeUserConfigJSON = applicationConfigElement.dataset.store_user_config;
const applicationConfig = JSON.parse( applicationConfigElement.dataset.config );
const accessToken = storeUserConfigJSON && JSON.parse( storeUserConfigJSON ).webapi_token;
if( !accessToken || !applicationConfig )
{
throw new Error( 'Failed to get application_config' );
}
/** @type {HTMLElement} */
let exploreButton;
/** @type {HTMLElement} */
let exploreStatus;
CreateExploreContainer();
function CreateExploreContainer()
{
const buttonContainer = document.createElement( 'div' );
buttonContainer.className = 'steamdb_cheat_queue discovery_queue_customize_ctn';
exploreButton = document.createElement( 'div' );
exploreButton.className = 'btnv6_blue_hoverfade btn_medium';
const span = document.createElement( 'span' );
span.textContent = _t( 'explore_auto_discover' );
exploreButton.append( span );
buttonContainer.append( exploreButton );
exploreStatus = document.createElement( 'div' );
exploreStatus.className = 'steamdb_cheat_queue_text';
exploreStatus.textContent = _t( 'explore_auto_discover_description' );
buttonContainer.append( exploreStatus );
const image = document.createElement( 'img' );
image.src = GetLocalResource( 'icons/white.svg' );
image.width = 32;
image.height = 32;
buttonContainer.append( image );
const container = document.querySelector( '.discovery_queue_customize_ctn' );
container.parentNode.insertBefore( buttonContainer, container );
exploreButton.addEventListener( 'click', ( ) =>
{
if( exploreButton.classList.contains( 'btn_disabled' ) )
{
return;
}
StartViewTransition( () =>
{
exploreStatus.textContent = _t( 'explore_generating' );
exploreButton.classList.add( 'btn_disabled' );
GenerateQueue();
} );
}, false );
}
function GenerateQueue( generateFails = 0 )
{
const valveQueueEl = document.getElementById( 'discovery_queue_ctn' );
if( valveQueueEl )
{
valveQueueEl.style.display = 'none';
}
if( emptyQueue )
{
emptyQueue.style.display = 'block';
}
const params = new URLSearchParams();
params.set( 'origin', location.origin );
params.set( 'access_token', accessToken );
params.set( 'country_code', applicationConfig.COUNTRY || 'US' );
params.set( 'rebuild_queue', '1' );
params.set( 'queue_type', '0' ); // k_EStoreDiscoveryQueueTypeNew
params.set( 'ignore_user_preferences', '1' );
fetch(
`${applicationConfig.WEBAPI_BASE_URL}IStoreService/GetDiscoveryQueue/v1/?${params.toString()}`,
)
.then( ( response ) =>
{
if( !response.ok )
{
throw new Error( `HTTP ${response.status}` );
}
return response.json();
} )
.then( ( data ) =>
{
if( !data.response || !data.response.appids )
{
throw new Error( 'Unexpected response' );
}
const appids = data.response.appids;
let done = 0;
let fails = 0;
/**
* @param {Response} response
*/
const requestDone = ( response ) =>
{
if( response.status !== 200 )
{
requestFail( new Error( `HTTP ${response.status}` ) );
return;
}
fails--;
if( ++done === appids.length )
{
exploreButton.classList.remove( 'btn_disabled' );
exploreStatus.textContent = _t( 'explore_finished' );
}
else
{
exploreStatus.textContent = _t( 'explore_exploring', [ done, appids.length ] );
requestNextInQueue( done );
}
};
/**
* @param {any} error
*/
const requestFail = ( error ) =>
{
WriteLog( 'Failed to clear queue item', error );
if( ++fails >= 10 )
{
exploreButton.classList.remove( 'btn_disabled' );
exploreStatus.textContent = _t( 'explore_failed_to_clear_too_many' );
return;
}
setTimeout( () =>
{
requestNextInQueue( done );
}, RandomInt( 5000, 10000 ) );
};
/**
* @param {number} index
*/
const requestNextInQueue = ( index ) =>
{
const skipParams = new URLSearchParams();
skipParams.set( 'origin', location.origin );
skipParams.set( 'access_token', accessToken );
skipParams.set( 'appid', appids[ index ] );
fetch(
`${applicationConfig.WEBAPI_BASE_URL}IStoreService/SkipDiscoveryQueueItem/v1/?${skipParams.toString()}`,
{
method: 'POST',
},
)
.then( requestDone )
.catch( requestFail );
};
requestNextInQueue( 0 );
} )
.catch( ( error ) =>
{
WriteLog( 'Failed to get discovery queue', error );
if( ++generateFails >= 20 )
{
exploreButton.classList.remove( 'btn_disabled' );
exploreStatus.textContent = _t( 'explore_failed_to_clear_too_many' );
return;
}
exploreStatus.textContent = `${_t( 'explore_generating' )} (#${generateFails})`;
setTimeout( () =>
{
GenerateQueue( generateFails );
}, RandomInt( 5000, 10000 * generateFails ) );
} );
}
/**
* @param {number} min
* @param {number} max
*/
function RandomInt( min, max )
{
return Math.floor( Math.random() * ( max - min + 1 ) + min );
}
/**
* @param {ViewTransitionUpdateCallback} callback
*/
function StartViewTransition( callback )
{
if( document.startViewTransition )
{
document.startViewTransition( () =>
{
try
{
callback();
}
catch( e )
{
console.error( e );
}
} );
}
else
{
callback();
}
}

View File

@@ -0,0 +1,30 @@
'use strict';
GetOption( { 'steamdb-highlight': true }, ( items ) =>
{
if( !items[ 'steamdb-highlight' ] )
{
return;
}
const element = document.createElement( 'script' );
element.id = 'steamdb_invalidate_cache';
element.type = 'text/javascript';
element.src = GetLocalResource( 'scripts/store/invalidate_cache_injected.js' );
document.head.appendChild( element );
window.addEventListener( 'message', ( request ) =>
{
if( request?.data && request.data.type === 'steamdb:extension-invalidate-cache' )
{
WriteLog( 'Invalidating userdata cache' );
SendMessageToBackgroundScript( {
contentScriptQuery: 'InvalidateCache',
}, () =>
{
// noop
} );
}
} );
} );

View File

@@ -0,0 +1,20 @@
'use strict';
( ( () =>
{
if( !window.GDynamicStore || !window.GDynamicStore.InvalidateCache )
{
return;
}
const originalGDynamicStoreInvalidateCache = window.GDynamicStore.InvalidateCache;
window.GDynamicStore.InvalidateCache = function SteamDB_GDynamicStore_InvalidateCache()
{
originalGDynamicStoreInvalidateCache.apply( this, arguments );
window.postMessage( {
type: 'steamdb:extension-invalidate-cache',
}, window.location.origin );
};
} )() );

View File

@@ -0,0 +1,24 @@
'use strict';
GetOption( { 'enhancement-market-ssa': true }, ( items ) =>
{
if( items[ 'enhancement-market-ssa' ] )
{
/** @type {HTMLInputElement} */
const element = document.querySelector( '#accept_ssa' );
if( element )
{
element.checked = true;
}
}
} );
const element = document.createElement( 'script' );
element.id = 'steamdb_registerkey_hook';
element.type = 'text/javascript';
element.src = GetLocalResource( 'scripts/store/registerkey_injected.js' );
element.dataset.icon = GetLocalResource( 'icons/white.svg' );
element.dataset.homepage = GetHomepage();
document.head.appendChild( element );

View File

@@ -0,0 +1,76 @@
'use strict';
( ( () =>
{
const script = document.getElementById( 'steamdb_registerkey_hook' );
const originalOnRegisterProductKeyFailure = window.OnRegisterProductKeyFailure;
/**
* @param {any} ePurchaseResult
* @param {any} receipt
*/
window.OnRegisterProductKeyFailure = function SteamDB_OnRegisterProductKeyFailure( ePurchaseResult, receipt )
{
originalOnRegisterProductKeyFailure.apply( this, arguments );
if( receipt?.line_items && receipt.line_items.length > 0 )
{
document.getElementById( 'error_display' ).appendChild( FormatLineItems( receipt.line_items ) );
}
};
/**
* @param {any} result
*/
window.UpdateReceiptForm = function SteamDB_UpdateReceiptForm( result )
{
const list = document.getElementById( 'registerkey_productlist' );
while( list.firstChild )
{
list.firstChild.remove();
}
list.appendChild( FormatLineItems( result.purchase_receipt_info.line_items ) );
};
/**
* @param {Record<string, any>[]} line_items
*/
function FormatLineItems( line_items )
{
const fragment = document.createElement( 'div' );
fragment.className = 'steamdb_registerkey_lineitem';
const image = document.createElement( 'img' );
image.src = script.dataset.icon;
for( const item of line_items )
{
const lineitem = document.createElement( 'div' );
lineitem.className = 'registerkey_lineitem';
lineitem.append( image );
let link = document.createElement( 'a' );
link.href = script.dataset.homepage + 'sub/' + item.packageid + '/';
link.rel = 'noreferrer';
link.target = '_blank';
link.appendChild( document.createTextNode( 'SteamDB' ) );
lineitem.append( link );
lineitem.appendChild( document.createTextNode( ' - ' ) );
link = document.createElement( 'a' );
const steamProtocol = window.location.hostname === 'store.steamchina.com' ? 'steamchina' : 'steam';
link.href = `${steamProtocol}://subscriptioninstall/${item.packageid}`;
link.appendChild( document.createTextNode( 'Install' ) );
lineitem.append( link );
lineitem.appendChild( document.createTextNode( ' - ' + item.line_item_description ) );
fragment.appendChild( lineitem );
}
return fragment;
}
} )() );

View File

@@ -0,0 +1,28 @@
'use strict';
GetOption( { 'button-sub': true }, ( items ) =>
{
if( !items[ 'button-sub' ] )
{
return;
}
const container = document.querySelector( '.game_meta_data' );
if( container )
{
let element = document.createElement( 'span' );
element.appendChild( document.createTextNode( _t( 'view_on_steamdb' ) ) );
const link = document.createElement( 'a' );
link.className = 'action_btn';
link.href = GetHomepage() + 'sub/' + GetCurrentAppID() + '/';
link.appendChild( element );
element = document.createElement( 'div' );
element.className = 'block';
element.appendChild( link );
container.insertBefore( element, container.firstChild );
}
} );

View File

@@ -0,0 +1,30 @@
'use strict';
( ( () =>
{
const scriptHook = document.getElementById( 'steamdb_subscriptions_hook' );
const homepage = scriptHook.dataset.homepage;
const originalDropdownSelectOption = window.GamePurchaseDropdownSelectOption;
/**
* @param {string} dropdownName
* @param {number} subId
*/
window.GamePurchaseDropdownSelectOption = function SteamDB_GamePurchaseDropdownSelectOption( dropdownName, subId )
{
originalDropdownSelectOption.apply( this, arguments );
const cartForm = document.getElementById( `add_to_cart_${dropdownName}` );
if( !cartForm )
{
return;
}
/** @type {HTMLAnchorElement} */
const link = cartForm.parentNode.querySelector( '.steamdb_link' );
link.hidden = false;
link.href = `${homepage}sub/${subId}/`;
link.querySelector( '.steamdb_link_id' ).textContent = subId.toString();
};
} )() );

View File

@@ -0,0 +1,62 @@
'use strict';
GetOption( { 'link-subid-widget': true }, ( items ) =>
{
if( !items[ 'link-subid-widget' ] )
{
return;
}
const link = document.createElement( 'a' );
link.className = 'btn_black btn_tiny steamdb_link';
link.target = '_blank';
const text = document.createElement( 'span' );
text.className = 'steamdb_link_id';
/** @type {HTMLInputElement} */
const subid = document.querySelector( 'input[name="subid"]' );
if( subid )
{
link.href = GetHomepage() + 'sub/' + subid.value + '/';
text.textContent = subid.value.toString();
}
else
{
const appid = GetCurrentAppID();
link.href = GetHomepage() + 'app/' + appid + '/';
text.textContent = appid.toString();
}
const span = document.createElement( 'span' );
span.dataset.tooltipText = _t( 'view_on_steamdb' );
const hash = document.createElement( 'span' );
hash.style.fontWeight = 'bold';
hash.textContent = '# ';
span.append( hash );
span.append( text );
link.append( span );
let platforms = document.querySelector( '.game_area_purchase_platform' );
if( !platforms )
{
platforms = document.createElement( 'div' );
platforms.className = 'game_area_purchase_platform';
const widget = document.getElementById( 'widget' );
if( !widget )
{
return;
}
widget.append( platforms );
}
platforms.append( link );
} );

View File

@@ -0,0 +1,9 @@
interface Window {
[propName: string]: any;
}
declare namespace browser {
namespace runtime {
const OnInstalledReason: typeof chrome.runtime.OnInstalledReason;
}
}