adds .local/share/millennium and easyeffects

This commit is contained in:
2026-08-06 18:27:07 +02:00
parent c52d483074
commit 778c0155bd
2581 changed files with 339035 additions and 0 deletions
@@ -0,0 +1,2 @@
declare function DoAchievements(isPersonal: boolean): void;
declare function StartViewTransition(callback: ViewTransitionUpdateCallback): void;
@@ -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 );
};
} )() );