Tüm .config dosyaları ilk yedekleme
This commit is contained in:
@@ -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 );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
@@ -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 );
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 );
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
declare function DoAchievements(isPersonal: boolean): void;
|
||||
declare function StartViewTransition(callback: ViewTransitionUpdateCallback): void;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 );
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* global DoAchievements */
|
||||
|
||||
'use strict';
|
||||
|
||||
DoAchievements( true );
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
( ( () =>
|
||||
{
|
||||
if( 'AcceptAppHub' in window && 'Proceed' in window )
|
||||
{
|
||||
window.Proceed();
|
||||
}
|
||||
} )() );
|
||||
@@ -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 );
|
||||
} )() );
|
||||
@@ -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 } ) );
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
} );
|
||||
@@ -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 ),
|
||||
);
|
||||
} )() );
|
||||
@@ -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 );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
||||
@@ -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 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
@@ -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 );
|
||||
} );
|
||||
}
|
||||
} )() );
|
||||
@@ -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' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
@@ -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 );
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
} );
|
||||
@@ -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();
|
||||
};
|
||||
} )() );
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
@@ -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 ),
|
||||
);
|
||||
} )() );
|
||||
@@ -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 );
|
||||
} );
|
||||
}
|
||||
@@ -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' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
} );
|
||||
}
|
||||
@@ -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 );
|
||||
} );
|
||||
@@ -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 );
|
||||
} );
|
||||
@@ -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 );
|
||||
};
|
||||
} )() );
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
} );
|
||||
} );
|
||||
@@ -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 );
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
} )() );
|
||||
@@ -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 );
|
||||
}
|
||||
} );
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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' );
|
||||
} )() );
|
||||
@@ -0,0 +1 @@
|
||||
declare function AddLinksInErrorBox(container: Element): void;
|
||||
@@ -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 );
|
||||
}
|
||||
@@ -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 = '';
|
||||
}
|
||||
} );
|
||||
@@ -0,0 +1,10 @@
|
||||
/* global AddLinksInErrorBox */
|
||||
|
||||
'use strict';
|
||||
|
||||
const container = document.getElementById( 'error_box' );
|
||||
|
||||
if( container && GetCurrentAppID() > 0 )
|
||||
{
|
||||
AddLinksInErrorBox( container );
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
} );
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
} );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
@@ -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 );
|
||||
};
|
||||
} )() );
|
||||
@@ -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 );
|
||||
@@ -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;
|
||||
}
|
||||
} )() );
|
||||
@@ -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 );
|
||||
}
|
||||
} );
|
||||
@@ -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();
|
||||
};
|
||||
} )() );
|
||||
@@ -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 );
|
||||
} );
|
||||
@@ -0,0 +1,9 @@
|
||||
interface Window {
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
declare namespace browser {
|
||||
namespace runtime {
|
||||
const OnInstalledReason: typeof chrome.runtime.OnInstalledReason;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user