Advertisement
Guest User

Untitled

a guest
Apr 23rd, 2016
159
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.41 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Dota 2 & CSGO Lounge item price displayer
  3. // @namespace http://www.enygma.ro
  4. // @version 3.0.2
  5. // @author Enygma
  6. // @description Displays an item's price information from the Steam Community Market and helps to copy an item's name.
  7. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  8. // @include /^http(s)?://(www.)?dota2lounge.com//
  9. // @include /^http(s)?://(www.)?csgolounge.com//
  10. // @updateURL https://github.com/Enygma2002/d2l-price-displayer/raw/master/Dota_2_Lounge_item_price_displayer.user.js
  11. // @downloadURL https://github.com/Enygma2002/d2l-price-displayer/raw/master/Dota_2_Lounge_item_price_displayer.user.js
  12. // @supportURL https://github.com/Enygma2002/d2l-price-displayer/issues
  13. // @contributionURL https://github.com/Enygma2002/d2l-price-displayer#donations
  14. // @contributionAmount $5.00 / Game / Items
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_addStyle
  17. // @grant GM_getValue
  18. // @grant GM_setValue
  19. // ==/UserScript==
  20.  
  21. // Determine on which site is the script being executed (dota2lounge or csgolounge)
  22. if (document.URL.match(/^http(s)?:\/\/(www.)?dota2lounge.com\//)) {
  23. // Dota2 app ID on Steam's community market website.
  24. appID = 570;
  25.  
  26. // Generic item placeholder names used by the d2l website and not existing in the Steam Market.
  27. genericItemPlaceholderNames = ["Offers", "Any Common", "Any Uncommon", "Any Rare", "Any Mythical", "Any Legendary",
  28. "Any Ancient", "Any Immortal", "Real Money", "+ More", "Any Set"];
  29. } else if (document.URL.match(/^http(s)?:\/\/(www.)?csgolounge.com\//)) {
  30. // CS:GO app ID on Steam's community market website.
  31. appID = 730;
  32.  
  33. // Generic item placeholder names used by the csgolounge website and not existing in the Steam Market.
  34. genericItemPlaceholderNames = ["Any Offers", "Real Money", "Dota Items", "TF2 Items"];
  35. }
  36.  
  37. // A mapping of available currencies that the Steam Market supports and their individual features.
  38. var availableCurrencies = {
  39. "$" : { symbol: "USD", decimal: '.', id: 1 },
  40. "£" : { symbol: "GBP", decimal: '.', id: 2 },
  41. "€" : { symbol: "EUR", decimal: ',', id: 3 },
  42. "pуб." : { symbol: "RUB", decimal: ',', id: 5 },
  43. "R$" : { symbol: "BRL", decimal: ',', id: 7 },
  44. "¥" : { symbol: "JPY", decimal: '.', id: 8 },
  45. "kr" : { symbol: "NOK", decimal: ',', id: 9 },
  46. "Rp" : { symbol: "IDR", decimal: '.', id: 10 },
  47. "RM" : { symbol: "MYR", decimal: '.', id: 11 },
  48. "P" : { symbol: "PHP", decimal: '.', id: 12 },
  49. "S$" : { symbol: "SGD", decimal: '.', id: 13 },
  50. "฿" : { symbol: "THB", decimal: '.', id: 14 },
  51. "₫" : { symbol: "VND", decimal: '.', id: 15 },
  52. "₩" : { symbol: "KRW", decimal: '.', id: 16 },
  53. "TL" : { symbol: "TRY", decimal: ',', id: 17 },
  54. "₴" : { symbol: "UAH", decimal: ',', id: 18 },
  55. "Mex$" : { symbol: "MXN", decimal: '.', id: 19 },
  56. "CDN$" : { symbol: "CAD", decimal: '.', id: 20 },
  57. "A$" : { symbol: "AUD", decimal: '.', id: 21 },
  58. "NZ$" : { symbol: "NZD", decimal: '.', id: 22 }
  59. // Add more as Steam makes them available.
  60. };
  61.  
  62. // Validate a currency, otherwise default to USD.
  63. var validateCurrency = function(currency) {
  64. var result = "$";
  65.  
  66. if (availableCurrencies[currency]) {
  67. result = currency;
  68. }
  69.  
  70. return result;
  71. };
  72.  
  73. // Get the user selected currency, if available, otherwise (or if invalid) default on USD.
  74. var currentCurrency = validateCurrency(GM_getValue("currency"));
  75.  
  76. // Registers the currency selection box in the top right of the website.
  77. var attachCurrencySelector = function() {
  78. var headerElement = document.querySelector('header');
  79.  
  80. var currencySelector = document.createElement('div');
  81. currencySelector.setAttribute("class", "ddbtn currency");
  82.  
  83. var currencySelectorHTML = "<div>";
  84.  
  85. // Add the currently selected currency.
  86. var currentCurrencyInfo = availableCurrencies[currentCurrency];
  87. currencySelectorHTML += "<a id='" + currentCurrency + "'>" + currentCurrencyInfo.symbol + "</a>";
  88.  
  89. // Add all the other available currencies to select from.
  90. var availableCurrencyCodes = Object.keys(availableCurrencies);
  91. for (var i=0; i<availableCurrencyCodes.length; i++) {
  92. var currencyCode = availableCurrencyCodes[i];
  93. // Skip the currently selected currency to avoid displaying it twice.
  94. if (currentCurrency === currencyCode) {
  95. continue;
  96. }
  97.  
  98. var currencyInfo = availableCurrencies[currencyCode];
  99. currencySelectorHTML += "<a id='" + currencyCode + "'>" + currencyInfo.symbol + "</a>";
  100. }
  101.  
  102. // Add the donations button.
  103. currencySelectorHTML += "<a id='donate' title='Show your appreciation for the script' ";
  104. currencySelectorHTML += "href='https://github.com/Enygma2002/d2l-price-displayer#donations' target='_blank'>Donate</a>";
  105.  
  106. currencySelectorHTML += "</div>";
  107.  
  108. currencySelector.innerHTML = currencySelectorHTML;
  109.  
  110. headerElement.appendChild(currencySelector);
  111.  
  112. // Attach click event listeners for all the available languages in the list.
  113. var availableCurrencyElements = currencySelector.querySelectorAll("a:not([id='donate'])");
  114. for (var i=0; i<availableCurrencyElements.length; i++) {
  115. var availableCurrencyElement = availableCurrencyElements[i];
  116. availableCurrencyElement.addEventListener('click', function(mouseEvent) {
  117. // Set the selected currency.
  118. var selectedCurrency = mouseEvent.target.id;
  119. setCurrentCurrency(selectedCurrency);
  120.  
  121. // Refresh the list of available languages to reflect the new selection.
  122. headerElement.removeChild(currencySelector);
  123. attachCurrencySelector();
  124.  
  125. // Refresh any already retrieved prices to use the newly selected currency.
  126. refreshExistingPrices();
  127. });
  128. }
  129. };
  130. attachCurrencySelector();
  131.  
  132. // Set the new current currency and save it so that it is available next time we reload the page.
  133. var setCurrentCurrency = function(newCurrentCurrency) {
  134. // Make sure we set a valid currency.
  135. var validatedNewCurrency = validateCurrency(newCurrentCurrency);
  136.  
  137. // Set the cached value used by the script.
  138. currentCurrency = validatedNewCurrency;
  139.  
  140. // Save the new value.
  141. GM_setValue("currency", validatedNewCurrency);
  142. };
  143.  
  144. // Refresh all the currently retrieved prices (that were hovered at some point) so that they are displayed consistenly
  145. // using the actual currentCurrency and not the currency that was active then they were first retrieved (hovered).
  146. var refreshExistingPrices = function() {
  147. var itemNameElements = document.querySelectorAll(".oitm > .name");
  148. for (var i=0; i<itemNameElements.length; i++) {
  149. var itemElement = itemNameElements[i].parentNode;
  150. if (itemElement.querySelector('.extraPanel')) {
  151. // Retrieve and override new values.
  152. getLowestPrice(itemElement, true);
  153. }
  154. }
  155. };
  156.  
  157. // Main event listener for hovering items.
  158. document.addEventListener("mouseover", function (event) {
  159. var itemElement = getItemElement(event);
  160. if (!itemElement) {
  161. return;
  162. }
  163.  
  164. attachExtraPanelAndListeners(itemElement);
  165. getLowestPrice(itemElement);
  166. });
  167.  
  168. // Get the hovered item, if any.
  169. var getItemElement = function(mouseEvent) {
  170. var targetElement = mouseEvent.target;
  171. var itemElement = null;
  172.  
  173. // Check the hovered element to see if it's an item element or one of it's children (image or rarity section).
  174.  
  175. // Old d2l website layout that used directly the "item" display and within it the "name" panel;
  176. // It is now only present on csgolounge but we still want to support it in order to work with both sites.
  177. if (hasClass(targetElement, "item") && targetElement.querySelector(".name")) {
  178. // Hover the item display (contains image and rarity section)
  179. itemElement = targetElement;
  180. } else if (hasClass(targetElement.parentNode, "item") && targetElement.parentNode.querySelector(".name")) {
  181. // Hover an individual item display's children (image or rarity section)
  182. itemElement = targetElement.parentNode;
  183. } else
  184. // New d2l website layout, that uses an "oitm" container for the "item" display and the "name" panel
  185. if (hasClass(targetElement, "oitm")) {
  186. // Hover the main container (contains item display and item popup).
  187. itemElement = targetElement;
  188. } else if (hasClass(targetElement.parentNode, "oitm")) {
  189. // Hover the item display (contains image and rarity section)
  190. itemElement = targetElement.parentNode;
  191. } else if (hasClass(targetElement.parentNode, "item")) {
  192. // Hover an individual item display's children (image or rarity section)
  193. itemElement = targetElement.parentNode.parentNode;
  194. } else {
  195. // Hovered an uninteresting element of the UI.
  196. return null;
  197. }
  198.  
  199. // Avoid returning empty item slots.
  200. var itemNameElement = itemElement.querySelector(".name");
  201. if (!itemNameElement) {
  202. return null;
  203. }
  204.  
  205. // Avoid returning generic item placeholders.
  206. var itemName = getItemName(itemElement);
  207. if (genericItemPlaceholderNames.indexOf(itemName) > -1) {
  208. return null;
  209. }
  210.  
  211. return itemElement;
  212. };
  213.  
  214. // 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
  215. var attachExtraPanelAndListeners = function(itemElement) {
  216. var itemNamePanel = itemElement.querySelector(".name");
  217. // If the extra panel already exists, stop here.
  218. var extraPanel = itemNamePanel.querySelector(".extraPanel");
  219. if (extraPanel) {
  220. return;
  221. }
  222.  
  223. // Otherwise, create our own panel to append...
  224. extraPanel = document.createElement('div');
  225. extraPanel.innerHTML = "<span class='scriptStatus'>Ready</span>" +
  226. "<button type='button' class='extraButton refreshButton' title='Refresh'/>";
  227. extraPanel.setAttribute("class", "extraPanel");
  228.  
  229. // ...and append it.
  230. // Note: In case we are in the "My Trades" view, make sure to insert before the remove button.
  231. var insertBeforeElement = itemNamePanel.querySelector("a.button");
  232. itemNamePanel.insertBefore(extraPanel, insertBeforeElement);
  233. // Set click event handler for the item's name panel so that the item name can be copied to the clipboard easier.
  234. itemNamePanel.addEventListener("click", copyItemNameHandler, false);
  235. // Set click event handler for the refresh button that re-fetches the item's price.
  236. var refreshButton = extraPanel.querySelector(".refreshButton");
  237. refreshButton.addEventListener("click", function(event) {
  238. event.stopPropagation();
  239. getLowestPrice(itemElement, true);
  240. }, false);
  241. };
  242.  
  243. // Get the lowest price for an item from the Steam market.
  244. var getLowestPrice = function(itemElement, override) {
  245. var itemNameElement = itemElement.querySelector(".name");
  246. // Don`t try to get the price if we've already retrieved it.
  247. if (!override && itemNameElement.querySelector(".scriptStatus").innerHTML != "Ready") {
  248. return;
  249. }
  250.  
  251. itemNameElement.querySelector(".scriptStatus").innerHTML = "Loading...";
  252. var itemName = getItemName(itemElement);
  253. var url = getSteamMarketListingsURL(itemName);
  254. GM_xmlhttpRequest({
  255. method: "GET",
  256. url: url,
  257. onload: function (response) {
  258. var jsonResponse = response.responseText;
  259. var priceObj = eval("(" + jsonResponse + ")");
  260. var result = "<span class='itemNotMarketable'>Not Marketable</span>";
  261. // Get only valid responses, since we can sometimes see JSONs with success: true and nothing else.
  262. if (priceObj.success && priceObj.lowest_price) {
  263. // Extra information to be displayed when hovering the price.
  264. var extraInfo = "Median price: " + (priceObj.median_price || "N/A") + "\n";
  265. extraInfo += "Daily transactions: " + (priceObj.volume || "N/A");
  266.  
  267. // Determine the trend, if possible.
  268. var lowestPrice = getPriceValue(priceObj.lowest_price);
  269. var medianPrice = getPriceValue(priceObj.median_price);
  270. var trend = null;
  271. if (lowestPrice && medianPrice) {
  272. if (lowestPrice > medianPrice) {
  273. // Ascending trend.
  274. trend = "&#8599;";
  275. } else if (lowestPrice < medianPrice) {
  276. // Descending trend.
  277. trend = "&#8600;";
  278. } else {
  279. // Stagnating trend.
  280. trend = "&#8594;";
  281. }
  282. }
  283.  
  284. // Determine price safety
  285. var priceSafety = getPriceSafety(priceObj.volume);
  286.  
  287. // Build the result.
  288. result = "<span class='itemMarketable " + priceSafety + "' title='" + extraInfo + "'>";
  289. result += priceObj.lowest_price;
  290. if (trend) {
  291. result += " " + trend;
  292. }
  293. result += "</span>";
  294. }
  295.  
  296. // Set the result.
  297. itemNameElement.querySelector(".scriptStatus").innerHTML = result;
  298. },
  299. onerror: function (response) {
  300. // Determine the error and build the result.
  301. var reason = response.statusText;
  302. var result = "<span class='error' title='" + reason + "'>Error</span>";
  303.  
  304. // Set the result.
  305. itemNameElement.querySelector(".scriptStatus").innerHTML = result;
  306. }
  307. });
  308. };
  309.  
  310. // Cached RegExps used by getPriceValue.
  311. var currencySymbolRegex = new RegExp("&#[0-9]+;");
  312. var numberSanitizingRegex = new RegExp("[^0-9]");
  313.  
  314. // Extract the numeric value from a currency-value string combination.
  315. var getPriceValue = function(priceWithCurrency) {
  316. // Some prices are not available for certain items.
  317. // Example: Dota2's Voracious Greevil displays only lowest_price, nothing else.
  318. if (!priceWithCurrency) {
  319. return null;
  320. }
  321.  
  322. // Sanitize the input string since it is html escaped in the returned JSON.
  323. priceWithCurrency = unescapeHtml(priceWithCurrency);
  324.  
  325. // Strip out the currency symbol and any spaces.
  326. var priceString = priceWithCurrency.replace(currentCurrency, '').trim();
  327.  
  328. // Strip out any thousand marker from the price, since parseFloat might get confused on some locales.
  329. var currencyInfo = availableCurrencies[currentCurrency];
  330. var decimalIndex = priceString.lastIndexOf(currencyInfo.decimal);
  331. var integerValue = priceString.substring(0, decimalIndex).replace(numberSanitizingRegex, '');
  332. var fractionalValue = priceString.substring(decimalIndex + 1);
  333. // When rebuilding the cleaned priceString, always use '.' since that is the only thing parseFloat understands.
  334. priceString = integerValue + '.' + fractionalValue;
  335.  
  336. // Parse and return the value.
  337. return parseFloat(priceString);
  338. };
  339.  
  340. // Helper method to unescape an escaped HTML string, such as what we get from the price JSON.
  341. var unescapeHtml = function(escapedStr) {
  342. var div = document.createElement("div");
  343. div.innerHTML = escapedStr;
  344. var child = div.childNodes[0];
  345.  
  346. return child ? child.nodeValue : "";
  347. };
  348.  
  349. // Cached RegExp used in getPriceSafety.
  350. var thousandSeparatorRegex = new RegExp("[,.]");
  351.  
  352. /*
  353. * If price volume information is available (nr. of daily transactions), we determine price "safety" based on how many
  354. * transactions are for the item. Few transactions (<= 10) means the item is not popular and it will be harder to get rid of.
  355. *
  356. * If no volume information exists, the default is "priceUnsafe".
  357. */
  358. var getPriceSafety = function(priceVolumeString) {
  359. var result = "priceUnsafe";
  360.  
  361. if (priceVolumeString) {
  362. // Make sure to clean the volume string of any thousand separator that can cause problems in the conversion.
  363. var dailyTransactions = parseInt(priceVolumeString.replace(thousandSeparatorRegex, ""));
  364.  
  365. // This is where we actually determine the "safety" of a price.
  366. if (dailyTransactions <= 10) {
  367. result = "priceUnsafe";
  368. } else if (dailyTransactions <= 50) {
  369. result = "priceWarning";
  370. } else {
  371. result = "priceSafe";
  372. }
  373. }
  374.  
  375. return result;
  376. };
  377.  
  378. // Computes the URL used to access the Steam market listings for a given item.
  379. var getSteamMarketListingsURL = function(itemName) {
  380. var itemNameEncoded = encodeURIComponent(itemName);
  381. var currencyInfo = availableCurrencies[currentCurrency];
  382.  
  383. var url = "http://steamcommunity.com/market/priceoverview/?appid=" + (appID);
  384. url += "&market_hash_name=" + itemNameEncoded;
  385. url += "&currency=" + currencyInfo.id;
  386.  
  387. return url;
  388. };
  389.  
  390. // Extract the item's name from a DOM item element.
  391. var getItemName = function(itemElement) {
  392. var itemNameElement = itemElement.querySelector(".name");
  393. var itemName = itemNameElement.querySelector("b").innerHTML.trim();
  394.  
  395. return itemName;
  396. };
  397.  
  398. // Event handler to facilitate copying an item's name.
  399. var copyItemNameHandler = function(event) {
  400. var clickedElement = event.target;
  401.  
  402. // Avoid executing this handler if the "Remove item" button is clicked in a trade.
  403. if (excludedTags.indexOf(clickedElement.tagName) > -1 || excludedTags.indexOf(clickedElement.parentNode.tagName) > -1) {
  404. return;
  405. }
  406.  
  407. // Stop the element's parent (item) from getting the click event. This stops the item from being selected.
  408. event.stopPropagation();
  409.  
  410. // Make sure we select the item name element.
  411. var itemNameElement = clickedElement;
  412. while (!hasClass(itemNameElement, "name")) {
  413. itemNameElement = itemNameElement.parentNode;
  414. }
  415.  
  416. // Get and display the item's name.
  417. var itemName = itemNameElement.querySelector("b").innerHTML.trim();
  418. window.prompt("Press CTRL+C to copy the item's name:", itemName);
  419. };
  420.  
  421. // Tags that, if clicked on in an item name panel, should not execute the copyItemNameHandler.
  422. var excludedTags = ["A", "IMG"];
  423.  
  424. // Helper method to check if an element has the specified class name.
  425. var hasClass = function(element, cls) {
  426. return element && (" " + element.className + " ").indexOf(" " + cls + " ") > -1;
  427. };
  428.  
  429. // Style.
  430. // The two websites currently have diferent styles, so we need to tweak a bit the price colors to make them properly readable.
  431. var priceSafeColor;
  432. var priceWarningColor;
  433. if (appID == 570) {
  434. // D2L - lighter colors to go with the darker name panel background.
  435. priceSafeColor = "#8EC13E"; // same used on the "Market" link
  436. priceWarningColor = "orange";
  437. } else {
  438. // CSGOLounge - darker colors to contrast with the lighter name panel background.
  439. priceSafeColor = "#32D732";
  440. priceWarningColor = "#FF1919";
  441. }
  442. GM_addStyle(".currency a { color: white; font-weight: bold; margin-top: 0.25em; }");
  443. GM_addStyle(".priceSafe { color: " + priceSafeColor + " }");
  444. GM_addStyle(".priceWarning { color : " + priceWarningColor + " }");
  445. GM_addStyle(".priceUnsafe { color : red }");
  446. GM_addStyle(".itemNotMarketable { color : red }");
  447. GM_addStyle(".error { color : red }");
  448. GM_addStyle(".extraButton { margin-left: 0.3em; vertical-align: top; margin-top: -0.1em; border: 0; padding: 0; width: 16px; height: 16px; }");
  449. GM_addStyle(".refreshButton { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABOUlEQVQ4jc2RsUoDQRCGv32CXECzdjaWRiOCVSA+RdqAL6BFesUXOPUFbCWKJ2thkcRgxCa3cJUEQuCwExRjCi1sxiKXsElO6wz81e58888/sPhlESwxlhNaeP/+zRnO/wCMNaBDIbVZG/ztppLcLYdpgK3uSFgGc05WAnbX7pTcD5FCQ8lyMDOlQ4mQaO8lcRI6Q7wATxsGR32k9YUc9RFtiL1gZsoTq1jk7D3JxLEeFNtKLj6ZqNhWkppHSOvxO3GRFlb3J3mc2VEb/I2mktM3Jtp5UKINgUuProYJoMO+C8jWyGhDXO0hl0Ok2hutma2RcR1UsMjx6ySoA9fJkqGUryu5+UDydSW5azbn1wiJyjFSjp3bO4lrg19opJzacZEhJMIi688juYBkFT+9eRpUGYOmbr6Q9QvwBrFqSdh8NgAAAABJRU5ErkJggg==) no-repeat left center; }");
  450. GM_addStyle(".ddbtn a#donate { font-size: 0.7em; margin-top: 1.5em; color: orange; }");
  451. 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; }");
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement