CheapShark Steam Integration

Adds current pricing info from CheapShark for other stores to the Steam Store

  1. // ==UserScript==
  2. // @name CheapShark Steam Integration
  3. // @description Adds current pricing info from CheapShark for other stores to the Steam Store
  4. // @version 0.3.0
  5. // @author Phlebiac
  6. // @match https://store.steampowered.com/app/*
  7. // @connect www.cheapshark.com
  8. // @run-at document-end
  9. // @noframes
  10. // @license MIT; https://opensource.org/licenses/MIT
  11. // @namespace Phlebiac/CheapShark
  12. // @icon https://www.cheapshark.com/img/icons/round_114.png
  13. // @grant GM_addStyle
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_xmlhttpRequest
  17. // @grant unsafeWindow
  18. // ==/UserScript==
  19.  
  20. ;(async () => {
  21. 'use strict'
  22.  
  23. const BASE_URL = 'https://www.cheapshark.com';
  24. const API_URL = `${BASE_URL}/api/1.0/`;
  25. const METACRITIC_BASE = 'https://www.metacritic.com';
  26.  
  27. let userPrefs = {
  28. open_in_new_tab: GM_getValue('open_in_new_tab', true),
  29. show_metacritic: GM_getValue('show_metacritic', true),
  30. store_info_cache: GM_getValue('store_info_cache', []),
  31. }
  32.  
  33. const appId = getCurrentAppId();
  34. if (!appId) {
  35. return;
  36. }
  37. injectCSS();
  38.  
  39. if (!userPrefs.store_info_cache.length) {
  40. log('Requesting store data...');
  41. GM_xmlhttpRequest({
  42. method: 'GET',
  43. url: `${API_URL}stores`,
  44. onload: buildStoreCache
  45. });
  46. } else {
  47. //log(`Using cache of ${userPrefs.store_info_cache.length} stores`);
  48. }
  49.  
  50. GM_xmlhttpRequest({
  51. method: 'GET',
  52. url: `${API_URL}deals?steamAppID=${appId}&sortBy=Price`,
  53. onload: addDealsToStorePage
  54. })
  55.  
  56. function getCurrentAppId() {
  57. const urlPath = window.location.pathname;
  58. const appId = urlPath.match(/\/app\/(\d+)/);
  59. if (appId === null) {
  60. log('Unable to get AppId from URL path:', urlPath);
  61. return false;
  62. }
  63. return appId[1];
  64. }
  65.  
  66. function buildStoreCache(response) {
  67. if (response.status === 200) {
  68. try {
  69. var stores = JSON.parse(response.responseText);
  70. log(`Caching data for ${stores.length} stores`);
  71. stores.forEach(store => { userPrefs.store_info_cache[store.storeID] = store; });
  72. GM_setValue('store_info_cache', userPrefs.store_info_cache);
  73. } catch (err) {
  74. log('Unable to parse CheapShark response as JSON:', response);
  75. log('Javascript error:', err);
  76. }
  77. } else {
  78. log('Got unexpected HTTP code from CheapShark:', response.status);
  79. }
  80. }
  81.  
  82. function addDealsToStorePage(response) {
  83. if (response.status === 200) {
  84. try {
  85. var deals = JSON.parse(response.responseText);
  86. //log(`Found ${deals.length} deals`);
  87. } catch (err) {
  88. log('Unable to parse CheapShark response as JSON:', response);
  89. log('Javascript error:', err);
  90. }
  91. } else {
  92. log('Got unexpected HTTP code from CheapShark:', response.status);
  93. }
  94. let target = document.querySelector('.game_purchase_action');
  95. if (target && deals.length > 0) {
  96. var bestDeal = deals[0];
  97. let node = Object.assign(document.createElement('span'), {
  98. className: 'cheapshark_deals_row btnv6_blue_hoverfade btn_medium'
  99. });
  100. if (userPrefs.show_metacritic && bestDeal.metacriticScore > 0) {
  101. node.appendChild(showMetaCritic(bestDeal));
  102. }
  103. node.appendChild(lowestPrice(bestDeal));
  104. if (deals.length > 1) {
  105. node.appendChild(listDeals(deals));
  106. }
  107. target.insertBefore(node, target.firstChild);
  108. target.insertBefore(preferencesDialog(), node);
  109. }
  110. }
  111.  
  112. function showMetaCritic(deal) {
  113. let node = Object.assign(document.createElement('a'), {
  114. className: 'cheapshark_metacritic',
  115. href: `${METACRITIC_BASE}${deal.metacriticLink}`,
  116. title: 'View on MetaCritic',
  117. target: userPrefs.open_in_new_tab ? '_blank' : '_self'
  118. });
  119. node.appendChild(Object.assign(document.createElement('img'), {
  120. src: `${METACRITIC_BASE}/MC_favicon.png`,
  121. className: 'cheapshark_metacritic_img',
  122. height: 20
  123. }));
  124. node.appendChild(Object.assign(document.createElement('span'), {
  125. textContent: `${deal.metacriticScore}`,
  126. className: `cheapshark_metacritic_score`
  127. }));
  128. return node;
  129. }
  130.  
  131. function lowestPrice(deal) {
  132. var store = userPrefs.store_info_cache[deal.storeID];
  133. let node = Object.assign(document.createElement('a'), {
  134. className: 'cheapshark_lowest',
  135. href: `${BASE_URL}/redirect?dealID=${deal.dealID}`,
  136. title: `$${deal.salePrice} on ${store.storeName}`,
  137. target: userPrefs.open_in_new_tab ? '_blank' : '_self'
  138. });
  139. node.appendChild(Object.assign(document.createElement('span'), {
  140. textContent: `Lowest: $${deal.salePrice}`,
  141. className: `cheapshark_lowest_price`,
  142. }));
  143. node.appendChild(Object.assign(document.createElement('img'), {
  144. src: `${BASE_URL}${store.images.icon}`,
  145. className: 'cheapshark_lowest_img'
  146. }));
  147. return node;
  148. }
  149.  
  150. function listDeals(deals) {
  151. // Build out a menu option for each store deal
  152. let dealOptions = '';
  153. for (const deal of deals) {
  154. var store = userPrefs.store_info_cache[deal.storeID];
  155. dealOptions = `${dealOptions}
  156. <div class="cheapshark_deal_option queue_menu_option">
  157. <div class="cheapshark_valign queue_menu_option_label">
  158. <a href="${BASE_URL}/redirect?dealID=${deal.dealID}" class="option_title">
  159. <img class="cheapshark_valign" src="${BASE_URL}${store.images.icon}"> $${deal.salePrice} on ${store.storeName}
  160. </a>
  161. </div>
  162. </div>`;
  163. }
  164. return Object.assign(document.createElement('div'), {
  165. className: 'cheapshark_deals_dropdown queue_control_button queue_btn_menu',
  166. innerHTML: `
  167. <div class="queue_menu_arrow">
  168. <span><img src="https://store.cloudflare.steamstatic.com/public/images/v6/btn_arrow_down_padded.png"></span>
  169. </div>
  170. <div class="cheapshark_menu_flyout queue_menu_flyout">
  171. <div class="queue_flyout_content">${dealOptions}
  172. </div>
  173. </div>`
  174. });
  175. }
  176.  
  177. function preferencesDialog() {
  178. const container = Object.assign(document.createElement('span'), {
  179. className: 'cheapshark_prefs_icon',
  180. title: 'Preferences for CheapShark Steam Integration',
  181. textContent: '⚙'
  182. })
  183.  
  184. container.addEventListener('click', () => {
  185. const html = `
  186. <div class="cheapshark_prefs">
  187. <div class="newmodal_prompt_description">
  188. New preferences will only take effect after you refresh the page.
  189. </div>
  190. <blockquote>
  191. <div>
  192. <input type="checkbox" id="cheapshark_open_in_new_tab" ${ userPrefs.open_in_new_tab ? 'checked' : '' } />
  193. <label for="cheapshark_open_in_new_tab">Open links in a new tab</label>
  194. </div>
  195. <div>
  196. <input type="checkbox" id="cheapshark_show_metacritic" ${ userPrefs.show_metacritic ? 'checked' : '' } />
  197. <label for="cheapshark_show_metacritic">Show MetaCritic score and link</label>
  198. </div>
  199. </blockquote>
  200. </div>`
  201.  
  202. unsafeWindow.ShowDialog('CheapShark on Steam Prefs', html);
  203.  
  204. // Handle preferences changes
  205. const inputs = document.querySelectorAll('.cheapshark_prefs input');
  206.  
  207. for (const input of inputs) {
  208. input.addEventListener('change', event => {
  209. const target = event.target;
  210. const prefName = target.id.replace('cheapshark_', '');
  211.  
  212. switch (target.type) {
  213. case 'text':
  214. userPrefs[prefName] = target.value;
  215. GM_setValue(prefName, target.value);
  216. break;
  217. case 'checkbox':
  218. userPrefs[prefName] = target.checked;
  219. GM_setValue(prefName, target.checked);
  220. break;
  221. default:
  222. break;
  223. }
  224. })
  225. }
  226. })
  227.  
  228. return container;
  229. }
  230.  
  231. function injectCSS() {
  232. GM_addStyle(`
  233. .cheapshark_deals_row {
  234. border-style: solid;
  235. border-color: black;
  236. //padding: 0 0 2px 5px;
  237. padding: 5px 5px 5px 5px;
  238. background: #4d4b49;
  239. }
  240. .cheapshark_deals_row a:hover {
  241. color: white;
  242. }
  243. .cheapshark_metacritic {
  244. margin-right: 6px;
  245. }
  246. .cheapshark_metacritic_img {
  247. vertical-align: middle;
  248. margin-right: 4px;
  249. }
  250. .cheapshark_metacritic_score {
  251. font-size: 16px;
  252. vertical-align: middle;
  253. }
  254. .cheapshark_lowest {
  255. padding: 0 6px 0 6px;
  256. }
  257. .cheapshark_lowest_img {
  258. vertical-align: middle;
  259. }
  260. .cheapshark_lowest_price {
  261. font-size: 14px;
  262. vertical-align: middle;
  263. padding-right: 5px;
  264. }
  265. .cheapshark_deals_dropdown {
  266. //margin: -6px 0 -6px 0;
  267. }
  268. .cheapshark_menu_flyout {
  269. position: relative !important;
  270. width: unset !important;
  271. padding: 6px 6px;
  272. margin: -34px -6px -6px -240px;
  273. //position: absolute;
  274. //left: 0px;
  275. //margin-top: 5px;
  276. }
  277. .cheapshark_deal_option {
  278. padding: 2px !important;
  279. }
  280. .cheapshark_valign {
  281. vertical-align: middle;
  282. }
  283. .cheapshark_prefs_icon {
  284. font-size: 20px;
  285. padding: 0 2px;
  286. //vertical-align: top;
  287. cursor: pointer;
  288. }
  289. .cheapshark_prefs input[type="checkbox"], .cheapshark_prefs label {
  290. line-height: 20px;
  291. vertical-align: middle;
  292. display: inline-block;
  293. color: #66c0f4;
  294. cursor: pointer;
  295. }
  296. .cheapshark_prefs blockquote {
  297. margin: 15px 0 5px 10px;
  298. }`)
  299. }
  300.  
  301. function log() {
  302. console.log('[CheapShark Steam Integration]', ...arguments)
  303. }
  304. })()