NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Dota 2 & CSGO Lounge item price displayer // @namespace http://www.enygma.ro // @version 3.0.2 // @author Enygma // @description Displays an item's price information from the Steam Community Market and helps to copy an item's name. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html // @include /^http(s)?://(www.)?dota2lounge.com// // @include /^http(s)?://(www.)?csgolounge.com// // @updateURL https://github.com/Enygma2002/d2l-price-displayer/raw/master/Dota_2_Lounge_item_price_displayer.user.js // @downloadURL https://github.com/Enygma2002/d2l-price-displayer/raw/master/Dota_2_Lounge_item_price_displayer.user.js // @supportURL https://github.com/Enygma2002/d2l-price-displayer/issues // @contributionURL https://github.com/Enygma2002/d2l-price-displayer#donations // @contributionAmount $5.00 / Game / Items // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // ==/UserScript== // Determine on which site is the script being executed (dota2lounge or csgolounge) if (document.URL.match(/^http(s)?:\/\/(www.)?dota2lounge.com\//)) { // Dota2 app ID on Steam's community market website. appID = 570; // Generic item placeholder names used by the d2l website and not existing in the Steam Market. genericItemPlaceholderNames = ["Offers", "Any Common", "Any Uncommon", "Any Rare", "Any Mythical", "Any Legendary", "Any Ancient", "Any Immortal", "Real Money", "+ More", "Any Set"]; } else if (document.URL.match(/^http(s)?:\/\/(www.)?csgolounge.com\//)) { // CS:GO app ID on Steam's community market website. appID = 730; // Generic item placeholder names used by the csgolounge website and not existing in the Steam Market. genericItemPlaceholderNames = ["Any Offers", "Real Money", "Dota Items", "TF2 Items"]; } // A mapping of available currencies that the Steam Market supports and their individual features. var availableCurrencies = { "$" : { symbol: "USD", decimal: '.', id: 1 }, "£" : { symbol: "GBP", decimal: '.', id: 2 }, "€" : { symbol: "EUR", decimal: ',', id: 3 }, "pуб." : { symbol: "RUB", decimal: ',', id: 5 }, "R$" : { symbol: "BRL", decimal: ',', id: 7 }, "¥" : { symbol: "JPY", decimal: '.', id: 8 }, "kr" : { symbol: "NOK", decimal: ',', id: 9 }, "Rp" : { symbol: "IDR", decimal: '.', id: 10 }, "RM" : { symbol: "MYR", decimal: '.', id: 11 }, "P" : { symbol: "PHP", decimal: '.', id: 12 }, "S$" : { symbol: "SGD", decimal: '.', id: 13 }, "฿" : { symbol: "THB", decimal: '.', id: 14 }, "₫" : { symbol: "VND", decimal: '.', id: 15 }, "₩" : { symbol: "KRW", decimal: '.', id: 16 }, "TL" : { symbol: "TRY", decimal: ',', id: 17 }, "₴" : { symbol: "UAH", decimal: ',', id: 18 }, "Mex$" : { symbol: "MXN", decimal: '.', id: 19 }, "CDN$" : { symbol: "CAD", decimal: '.', id: 20 }, "A$" : { symbol: "AUD", decimal: '.', id: 21 }, "NZ$" : { symbol: "NZD", decimal: '.', id: 22 } // Add more as Steam makes them available. }; // Validate a currency, otherwise default to USD. var validateCurrency = function(currency) { var result = "$"; if (availableCurrencies[currency]) { result = currency; } return result; }; // Get the user selected currency, if available, otherwise (or if invalid) default on USD. var currentCurrency = validateCurrency(GM_getValue("currency")); // Registers the currency selection box in the top right of the website. var attachCurrencySelector = function() { var headerElement = document.querySelector('header'); var currencySelector = document.createElement('div'); currencySelector.setAttribute("class", "ddbtn currency"); var currencySelectorHTML = "<div>"; // Add the currently selected currency. var currentCurrencyInfo = availableCurrencies[currentCurrency]; currencySelectorHTML += "<a id='" + currentCurrency + "'>" + currentCurrencyInfo.symbol + "</a>"; // Add all the other available currencies to select from. var availableCurrencyCodes = Object.keys(availableCurrencies); for (var i=0; i<availableCurrencyCodes.length; i++) { var currencyCode = availableCurrencyCodes[i]; // Skip the currently selected currency to avoid displaying it twice. if (currentCurrency === currencyCode) { continue; } var currencyInfo = availableCurrencies[currencyCode]; currencySelectorHTML += "<a id='" + currencyCode + "'>" + currencyInfo.symbol + "</a>"; } // Add the donations button. currencySelectorHTML += "<a id='donate' title='Show your appreciation for the script' "; currencySelectorHTML += "href='https://github.com/Enygma2002/d2l-price-displayer#donations' target='_blank'>Donate</a>"; currencySelectorHTML += "</div>"; currencySelector.innerHTML = currencySelectorHTML; headerElement.appendChild(currencySelector); // Attach click event listeners for all the available languages in the list. var availableCurrencyElements = currencySelector.querySelectorAll("a:not([id='donate'])"); for (var i=0; i<availableCurrencyElements.length; i++) { var availableCurrencyElement = availableCurrencyElements[i]; availableCurrencyElement.addEventListener('click', function(mouseEvent) { // Set the selected currency. var selectedCurrency = mouseEvent.target.id; setCurrentCurrency(selectedCurrency); // Refresh the list of available languages to reflect the new selection. headerElement.removeChild(currencySelector); attachCurrencySelector(); // Refresh any already retrieved prices to use the newly selected currency. refreshExistingPrices(); }); } }; attachCurrencySelector(); // Set the new current currency and save it so that it is available next time we reload the page. var setCurrentCurrency = function(newCurrentCurrency) { // Make sure we set a valid currency. var validatedNewCurrency = validateCurrency(newCurrentCurrency); // Set the cached value used by the script. currentCurrency = validatedNewCurrency; // Save the new value. GM_setValue("currency", validatedNewCurrency); }; // Refresh all the currently retrieved prices (that were hovered at some point) so that they are displayed consistenly // using the actual currentCurrency and not the currency that was active then they were first retrieved (hovered). var refreshExistingPrices = function() { var itemNameElements = document.querySelectorAll(".oitm > .name"); for (var i=0; i<itemNameElements.length; i++) { var itemElement = itemNameElements[i].parentNode; if (itemElement.querySelector('.extraPanel')) { // Retrieve and override new values. getLowestPrice(itemElement, true); } } }; // Main event listener for hovering items. document.addEventListener("mouseover", function (event) { var itemElement = getItemElement(event); if (!itemElement) { return; } attachExtraPanelAndListeners(itemElement); getLowestPrice(itemElement); }); // Get the hovered item, if any. var getItemElement = function(mouseEvent) { var targetElement = mouseEvent.target; var itemElement = null; // Check the hovered element to see if it's an item element or one of it's children (image or rarity section). // Old d2l website layout that used directly the "item" display and within it the "name" panel; // It is now only present on csgolounge but we still want to support it in order to work with both sites. if (hasClass(targetElement, "item") && targetElement.querySelector(".name")) { // Hover the item display (contains image and rarity section) itemElement = targetElement; } else if (hasClass(targetElement.parentNode, "item") && targetElement.parentNode.querySelector(".name")) { // Hover an individual item display's children (image or rarity section) itemElement = targetElement.parentNode; } else // New d2l website layout, that uses an "oitm" container for the "item" display and the "name" panel if (hasClass(targetElement, "oitm")) { // Hover the main container (contains item display and item popup). itemElement = targetElement; } else if (hasClass(targetElement.parentNode, "oitm")) { // Hover the item display (contains image and rarity section) itemElement = targetElement.parentNode; } else if (hasClass(targetElement.parentNode, "item")) { // Hover an individual item display's children (image or rarity section) itemElement = targetElement.parentNode.parentNode; } else { // Hovered an uninteresting element of the UI. return null; } // Avoid returning empty item slots. var itemNameElement = itemElement.querySelector(".name"); if (!itemNameElement) { return null; } // Avoid returning generic item placeholders. var itemName = getItemName(itemElement); if (genericItemPlaceholderNames.indexOf(itemName) > -1) { return null; } return itemElement; }; // Add to the specified item element an extra panel that contains the price information and a click handler to facilitate copying the item's name var attachExtraPanelAndListeners = function(itemElement) { var itemNamePanel = itemElement.querySelector(".name"); // If the extra panel already exists, stop here. var extraPanel = itemNamePanel.querySelector(".extraPanel"); if (extraPanel) { return; } // Otherwise, create our own panel to append... extraPanel = document.createElement('div'); extraPanel.innerHTML = "<span class='scriptStatus'>Ready</span>" + "<button type='button' class='extraButton refreshButton' title='Refresh'/>"; extraPanel.setAttribute("class", "extraPanel"); // ...and append it. // Note: In case we are in the "My Trades" view, make sure to insert before the remove button. var insertBeforeElement = itemNamePanel.querySelector("a.button"); itemNamePanel.insertBefore(extraPanel, insertBeforeElement); // Set click event handler for the item's name panel so that the item name can be copied to the clipboard easier. itemNamePanel.addEventListener("click", copyItemNameHandler, false); // Set click event handler for the refresh button that re-fetches the item's price. var refreshButton = extraPanel.querySelector(".refreshButton"); refreshButton.addEventListener("click", function(event) { event.stopPropagation(); getLowestPrice(itemElement, true); }, false); }; // Get the lowest price for an item from the Steam market. var getLowestPrice = function(itemElement, override) { var itemNameElement = itemElement.querySelector(".name"); // Don`t try to get the price if we've already retrieved it. if (!override && itemNameElement.querySelector(".scriptStatus").innerHTML != "Ready") { return; } itemNameElement.querySelector(".scriptStatus").innerHTML = "Loading..."; var itemName = getItemName(itemElement); var url = getSteamMarketListingsURL(itemName); GM_xmlhttpRequest({ method: "GET", url: url, onload: function (response) { var jsonResponse = response.responseText; var priceObj = eval("(" + jsonResponse + ")"); var result = "<span class='itemNotMarketable'>Not Marketable</span>"; // Get only valid responses, since we can sometimes see JSONs with success: true and nothing else. if (priceObj.success && priceObj.lowest_price) { // Extra information to be displayed when hovering the price. var extraInfo = "Median price: " + (priceObj.median_price || "N/A") + "\n"; extraInfo += "Daily transactions: " + (priceObj.volume || "N/A"); // Determine the trend, if possible. var lowestPrice = getPriceValue(priceObj.lowest_price); var medianPrice = getPriceValue(priceObj.median_price); var trend = null; if (lowestPrice && medianPrice) { if (lowestPrice > medianPrice) { // Ascending trend. trend = "↗"; } else if (lowestPrice < medianPrice) { // Descending trend. trend = "↘"; } else { // Stagnating trend. trend = "→"; } } // Determine price safety var priceSafety = getPriceSafety(priceObj.volume); // Build the result. result = "<span class='itemMarketable " + priceSafety + "' title='" + extraInfo + "'>"; result += priceObj.lowest_price; if (trend) { result += " " + trend; } result += "</span>"; } // Set the result. itemNameElement.querySelector(".scriptStatus").innerHTML = result; }, onerror: function (response) { // Determine the error and build the result. var reason = response.statusText; var result = "<span class='error' title='" + reason + "'>Error</span>"; // Set the result. itemNameElement.querySelector(".scriptStatus").innerHTML = result; } }); }; // Cached RegExps used by getPriceValue. var currencySymbolRegex = new RegExp("&#[0-9]+;"); var numberSanitizingRegex = new RegExp("[^0-9]"); // Extract the numeric value from a currency-value string combination. var getPriceValue = function(priceWithCurrency) { // Some prices are not available for certain items. // Example: Dota2's Voracious Greevil displays only lowest_price, nothing else. if (!priceWithCurrency) { return null; } // Sanitize the input string since it is html escaped in the returned JSON. priceWithCurrency = unescapeHtml(priceWithCurrency); // Strip out the currency symbol and any spaces. var priceString = priceWithCurrency.replace(currentCurrency, '').trim(); // Strip out any thousand marker from the price, since parseFloat might get confused on some locales. var currencyInfo = availableCurrencies[currentCurrency]; var decimalIndex = priceString.lastIndexOf(currencyInfo.decimal); var integerValue = priceString.substring(0, decimalIndex).replace(numberSanitizingRegex, ''); var fractionalValue = priceString.substring(decimalIndex + 1); // When rebuilding the cleaned priceString, always use '.' since that is the only thing parseFloat understands. priceString = integerValue + '.' + fractionalValue; // Parse and return the value. return parseFloat(priceString); }; // Helper method to unescape an escaped HTML string, such as what we get from the price JSON. var unescapeHtml = function(escapedStr) { var div = document.createElement("div"); div.innerHTML = escapedStr; var child = div.childNodes[0]; return child ? child.nodeValue : ""; }; // Cached RegExp used in getPriceSafety. var thousandSeparatorRegex = new RegExp("[,.]"); /* * If price volume information is available (nr. of daily transactions), we determine price "safety" based on how many * transactions are for the item. Few transactions (<= 10) means the item is not popular and it will be harder to get rid of. * * If no volume information exists, the default is "priceUnsafe". */ var getPriceSafety = function(priceVolumeString) { var result = "priceUnsafe"; if (priceVolumeString) { // Make sure to clean the volume string of any thousand separator that can cause problems in the conversion. var dailyTransactions = parseInt(priceVolumeString.replace(thousandSeparatorRegex, "")); // This is where we actually determine the "safety" of a price. if (dailyTransactions <= 10) { result = "priceUnsafe"; } else if (dailyTransactions <= 50) { result = "priceWarning"; } else { result = "priceSafe"; } } return result; }; // Computes the URL used to access the Steam market listings for a given item. var getSteamMarketListingsURL = function(itemName) { var itemNameEncoded = encodeURIComponent(itemName); var currencyInfo = availableCurrencies[currentCurrency]; var url = "http://steamcommunity.com/market/priceoverview/?appid=" + (appID); url += "&market_hash_name=" + itemNameEncoded; url += "¤cy=" + currencyInfo.id; return url; }; // Extract the item's name from a DOM item element. var getItemName = function(itemElement) { var itemNameElement = itemElement.querySelector(".name"); var itemName = itemNameElement.querySelector("b").innerHTML.trim(); return itemName; }; // Event handler to facilitate copying an item's name. var copyItemNameHandler = function(event) { var clickedElement = event.target; // Avoid executing this handler if the "Remove item" button is clicked in a trade. if (excludedTags.indexOf(clickedElement.tagName) > -1 || excludedTags.indexOf(clickedElement.parentNode.tagName) > -1) { return; } // Stop the element's parent (item) from getting the click event. This stops the item from being selected. event.stopPropagation(); // Make sure we select the item name element. var itemNameElement = clickedElement; while (!hasClass(itemNameElement, "name")) { itemNameElement = itemNameElement.parentNode; } // Get and display the item's name. var itemName = itemNameElement.querySelector("b").innerHTML.trim(); window.prompt("Press CTRL+C to copy the item's name:", itemName); }; // Tags that, if clicked on in an item name panel, should not execute the copyItemNameHandler. var excludedTags = ["A", "IMG"]; // Helper method to check if an element has the specified class name. var hasClass = function(element, cls) { return element && (" " + element.className + " ").indexOf(" " + cls + " ") > -1; }; // Style. // The two websites currently have diferent styles, so we need to tweak a bit the price colors to make them properly readable. var priceSafeColor; var priceWarningColor; if (appID == 570) { // D2L - lighter colors to go with the darker name panel background. priceSafeColor = "#8EC13E"; // same used on the "Market" link priceWarningColor = "orange"; } else { // CSGOLounge - darker colors to contrast with the lighter name panel background. priceSafeColor = "green"; priceWarningColor = "#C74411"; } GM_addStyle(".currency a { color: white; font-weight: bold; margin-top: 0.25em; }"); GM_addStyle(".priceSafe { color: " + priceSafeColor + " }"); GM_addStyle(".priceWarning { color : " + priceWarningColor + " }"); GM_addStyle(".priceUnsafe { color : red }"); GM_addStyle(".itemNotMarketable { color : red }"); GM_addStyle(".error { color : red }"); GM_addStyle(".extraButton { margin-left: 0.3em; vertical-align: top; margin-top: -0.1em; border: 0; padding: 0; width: 16px; height: 16px; }"); GM_addStyle(".refreshButton { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABOUlEQVQ4jc2RsUoDQRCGv32CXECzdjaWRiOCVSA+RdqAL6BFesUXOPUFbCWKJ2thkcRgxCa3cJUEQuCwExRjCi1sxiKXsElO6wz81e58888/sPhlESwxlhNaeP/+zRnO/wCMNaBDIbVZG/ztppLcLYdpgK3uSFgGc05WAnbX7pTcD5FCQ8lyMDOlQ4mQaO8lcRI6Q7wATxsGR32k9YUc9RFtiL1gZsoTq1jk7D3JxLEeFNtKLj6ZqNhWkppHSOvxO3GRFlb3J3mc2VEb/I2mktM3Jtp5UKINgUuProYJoMO+C8jWyGhDXO0hl0Ok2hutma2RcR1UsMjx6ySoA9fJkqGUryu5+UDydSW5azbn1wiJyjFSjp3bO4lrg19opJzacZEhJMIi688juYBkFT+9eRpUGYOmbr6Q9QvwBrFqSdh8NgAAAABJRU5ErkJggg==) no-repeat left center; }"); GM_addStyle(".ddbtn a#donate { font-size: 0.7em; margin-top: 1.5em; color: orange; }"); GM_addStyle(".ddbtn a#donate:before { color: red; content: '\u2665'; display: block; font-size: 1.5em; margin-bottom: 0.2em; text-align: center; text-shadow: 0 1px 1px #000; }");