Bundle Helper

Highlights games you already own on Steam, on other sites. Modified from https://gf.qytechs.cn/scripts/16105-bundle-helper/

  1. // ==UserScript==
  2. // @name Bundle Helper
  3. // @version 2.0.4
  4. // @author Dillon Regimbal
  5. // @namespace https://dillonr.com
  6. // @description Highlights games you already own on Steam, on other sites. Modified from https://gf.qytechs.cn/scripts/16105-bundle-helper/
  7. // @match *://cubicbundle.com/*
  8. // @match *://dailyindiegame.com/*
  9. // @match *://forums.steampowered.com/forums/showthread.php?*
  10. // @match *://www.gogobundle.com/latest/bundles/*
  11. // @match *://otakumaker.com/*
  12. // @match *://www.otakumaker.com/*
  13. // @match *://otakubundle.com/latest/bundles/*
  14. // @match *://steamcommunity.com/*/home*
  15. // @match *://steamcommunity.com/groups/*/announcements*
  16. // @match *://steamcompanion.com/gifts/*
  17. // @match *://steamground.com/*
  18. // @match *://store.steampowered.com/
  19. // @match *://store.steampowered.com/account/notinterested/*
  20. // @match *://store.steampowered.com/app/*
  21. // @match *://store.steampowered.com/widget/*
  22. // @match *://store.steampowered.com/search/*
  23. // @match *://whosgamingnow.net/*
  24. // @match *://www.bunchkeys.com/*
  25. // @match *://www.bundlekings.com/*
  26. // @match *://www.fanatical.com/*
  27. // @match *://www.dailyindiegame.com/*
  28. // @match *://www.gamebundle.com/*
  29. // @match *://www.hrkgame.com/*
  30. // @match *://www.humblebundle.com/*
  31. // @match *://www.indiegala.com/*
  32. // @match *://www.orlygift.com/*
  33. // @match *://www.reddit.com/r/*/comments/*
  34. // @match *://www.superduperbundle.com/*
  35. // @match *://www.sgtools.info/*
  36. // @match *://steamkeys.ovh/*
  37. // @match *://steamdb.info/*
  38. // @match *://itch.io/*
  39. // @match *://*.itch.io/*
  40. // @run-at document-start
  41. // @grant GM_addStyle
  42. // @grant GM_xmlhttpRequest
  43. // @grant GM_getValue
  44. // @grant GM_setValue
  45. // @connect store.steampowered.com
  46. // @connect www.hrkgame.com
  47. // @connect www.fanatical.com
  48. // @connect www.steamgifts.com
  49. // @icon https://store.steampowered.com/favicon.ico
  50. // @license GPL-3.0-only
  51. // @noframes
  52. // ==/UserScript==
  53.  
  54. // Connect to store.steampowered.com to get owner info
  55. // Connect to www.hrkgame.com and www.fanatical.com to get Steam ID of each products
  56. // Connect to www.steamgifts.com to get bundle threads
  57.  
  58. // License: GPL-3.0-only - https://spdx.org/licenses/GPL-3.0-only.html
  59.  
  60. // Since 2016-01-10
  61. // https://gf.qytechs.cn/scripts/16105-bundle-helper/
  62.  
  63. // Since 2020-05-21
  64. // https://gf.qytechs.cn/scripts/403878-bundle-helper
  65. // https://github.com/dregimbal/UserScripts/blob/master/BundleHelper.user.js
  66.  
  67. (function () {
  68. 'use strict'
  69.  
  70. let write_console_messages = true
  71.  
  72. let name_profile_json = 'bh_profile_json'
  73. let name_profile_time = 'bh_profile_time'
  74. let owned_item_class = 'bh_owned'
  75. let steam_profile = getSteamProfile()
  76.  
  77. let default_steam_url_selector = 'a[href*=\'store.steampowered.com/\']'
  78.  
  79. let divButton = document.createElement('div')
  80.  
  81. function attachOnLoad(callback) {
  82. window.addEventListener('load', function (e) {
  83. callback()
  84. })
  85. }
  86.  
  87. function attachOnReady(callback) {
  88. document.addEventListener('DOMContentLoaded', function (e) {
  89. callback()
  90. })
  91. }
  92.  
  93. function writeConsoleMessage(message) {
  94. if (write_console_messages) {
  95. console.log(message)
  96. }
  97. }
  98.  
  99. let timeoutList = []
  100. let intervalList = []
  101.  
  102. function setTimeoutCustom(func, tm, params) {
  103. let id = setTimeout(func, tm, params)
  104. timeoutList.push(id)
  105. return id
  106. }
  107.  
  108. function clearTimeoutAll() {
  109. for (let i = 0; i < timeoutList.length; i++) {
  110. clearTimeout(timeoutList[i])
  111. }
  112. }
  113.  
  114. function clearIntervalAll() {
  115. for (let i = 0; i < intervalList.length; i++) {
  116. clearInterval(intervalList[i])
  117. }
  118. }
  119.  
  120. function getUnixTimestamp() {
  121. return parseInt(Date.now() / 1000)
  122. }
  123.  
  124. function isProfileCacheExpired() {
  125. let isExpired = false
  126. let timestampExpired = 15 * 60
  127.  
  128. let profileTimestamp = GM_getValue(name_profile_time, 0)
  129.  
  130. let profileTimestampDiff = getUnixTimestamp() - profileTimestamp
  131. if (profileTimestampDiff > timestampExpired) {
  132. isExpired = true
  133. }
  134.  
  135. if (!isExpired) {
  136. writeConsoleMessage('Profile Cache Updated ' + profileTimestampDiff + 's ago')
  137. } else {
  138. writeConsoleMessage('Profile Cache Expired: ' + profileTimestampDiff)
  139. }
  140.  
  141. return isExpired
  142. }
  143.  
  144. function setProfileCache(json) {
  145. GM_setValue(name_profile_json, json)
  146. GM_setValue(name_profile_time, getUnixTimestamp())
  147. }
  148.  
  149. function getSteamProfile() {
  150. if (isProfileCacheExpired()) {
  151. updateSteamProfileCache()
  152. }
  153. return GM_getValue(name_profile_json, 0)
  154. }
  155.  
  156. function markOwned(query, getElementCallback, getProductIdCallback
  157. , classOwned, classNotInterested, classWished, getCountCallback) {
  158. if (!document.querySelector(query)) {
  159. // writeConsoleMessage("markOwned: empty");
  160. return
  161. }
  162.  
  163. if (!getElementCallback) {
  164. getElementCallback = function (ele, type) {
  165. // type -> 1: Owned, 2: Ignored, 3: Wishlist
  166. return ele
  167. }
  168. }
  169.  
  170. if (!getProductIdCallback) {
  171. getProductIdCallback = function (ele) {
  172. return ele.getAttribute('href')
  173. }
  174. }
  175.  
  176. if (!getCountCallback) {
  177. getCountCallback = function (appCount, subCount, appOwned, subOwned) {
  178. }
  179. }
  180.  
  181. if (!classOwned) {
  182. classOwned = ''
  183. }
  184. if (!classNotInterested) {
  185. classNotInterested = ''
  186. }
  187. if (!classWished) {
  188. classWished = ''
  189. }
  190.  
  191. let rgxId = /[0-9]{3,}/g
  192. let rgxApp = /((:\/\/(store\.steampowered\.com|steamcommunity\.com|steamdb\.info)(\/agecheck)?\/app|\/steam\/apps)\/[0-9]+|^[0-9]{3,}$)/i
  193. let rgxSub = /(:\/\/(store\.steampowered\.com|steamdb\.info)\/sub|\/steam\/subs)\/[0-9]+/i
  194.  
  195. let markFromJson = function (dataRes) {
  196. if (!dataRes) {
  197. writeConsoleMessage('markFromJson: empty')
  198. return
  199. }
  200.  
  201. let countOwned = [0, 0]
  202. let countAll = [0, 0]
  203.  
  204. let eleApps = document.querySelectorAll(query)
  205. writeConsoleMessage(eleApps)
  206. for (let i = 0; i < eleApps.length; i++) {
  207. let attrHref = getProductIdCallback(eleApps[i])
  208. let ids = attrHref.match(rgxId)
  209. if (ids) {
  210. // writeConsoleMessage('Matched ID "' + ids[0] + '" from url: ' + attrHref)
  211. let valId = parseInt(ids[0])
  212. if (rgxApp.test(attrHref)) {
  213. if (isAppOwned(valId)) {
  214. let ele = getElementCallback(eleApps[i], 1)
  215. if (ele && classOwned !== '') {
  216. ele.classList.add(classOwned)
  217. }
  218. countOwned[0]++
  219. } else if (isAppWishlisted(valId)) {
  220. let ele = getElementCallback(eleApps[i], 3)
  221. if (ele && classWished !== '') {
  222. ele.classList.add(classWished)
  223. }
  224. } else if (isAppIgnored(valId)) {
  225. let ele = getElementCallback(eleApps[i], 2)
  226. if (ele && classNotInterested !== '') {
  227. ele.classList.add(classNotInterested)
  228. }
  229. } else {
  230. // writeConsoleMessage('App: Unowned - https://store.steampowered.com/app/' + valId + '/')
  231. }
  232.  
  233. countAll[0]++
  234. } else if (rgxSub.test(attrHref)) {
  235. if (steam_profile.rgOwnedPackages.indexOf(valId) > -1) {
  236. writeConsoleMessage('Sub: owned - https://store.steampowered.com/sub/' + valId + '/')
  237. let ele = getElementCallback(eleApps[i], 1)
  238. if (ele && classOwned !== '') {
  239. ele.classList.add(classOwned)
  240. }
  241. countOwned[1]++
  242. } else {
  243. // writeConsoleMessage('Sub: not owned - https://store.steampowered.com/sub/' + valId + '/')
  244. }
  245. countAll[1]++
  246. } else {
  247. writeConsoleMessage('Cannot determine url type: ' + attrHref)
  248. }
  249. } else {
  250. writeConsoleMessage('Cannot match ID from url: ' + attrHref)
  251. }
  252. }
  253.  
  254. writeConsoleMessage('App: Owned ' + countOwned[0] + '/' + countAll[0])
  255. writeConsoleMessage('Sub: Owned ' + countOwned[1] + '/' + countAll[1])
  256.  
  257. getCountCallback(countAll[0], countAll[1], countOwned[0], countOwned[1])
  258. }
  259.  
  260. markFromJson(steam_profile)
  261. }
  262.  
  263. function updateSteamProfileCache() {
  264. GM_xmlhttpRequest(
  265. {
  266. method: 'GET',
  267. url: 'https://store.steampowered.com/dynamicstore/userdata/?t=' + getUnixTimestamp(),
  268. onload: function (response) {
  269. writeConsoleMessage('Steam User Data: ' + response.responseText.length + ' bytes')
  270.  
  271. let dataRes = JSON.parse(response.responseText)
  272.  
  273. setProfileCache(dataRes)
  274. steam_profile = dataRes
  275. }
  276. })
  277. }
  278.  
  279. // eslint-disable-next-line no-unused-vars
  280. function createCacheResetButton() {
  281. let divCacheResetButton = document.createElement('div')
  282. divCacheResetButton.classList.add('bh_button')
  283. divCacheResetButton.id = 'bh_cacheReset'
  284.  
  285. let cacheResetA = document.createElement('a')
  286. cacheResetA.setAttribute('onclick', 'return false;')
  287. cacheResetA.textContent = 'Reset Bundle Helper Cache'
  288.  
  289. divCacheResetButton.appendChild(cacheResetA)
  290. document.body.appendChild(divCacheResetButton)
  291.  
  292. divCacheResetButton.addEventListener('click',
  293. function () {
  294. updateSteamProfileCache()
  295. })
  296. }
  297.  
  298. function addMarkBtnHandler(onClickFunction, argsArray) {
  299. if (!document.body.contains(divButton)) {
  300. document.body.appendChild(divButton)
  301. }
  302.  
  303. divButton.addEventListener('click', () => {
  304. onClickFunction.apply(null, argsArray)
  305. })
  306. }
  307.  
  308. function setElementOwned(element) {
  309. if (typeof element !== 'undefined' && element !== null) {
  310. element.classList.add(owned_item_class)
  311. }
  312. }
  313.  
  314. /**
  315. * @description Checks the Steam game's ID against the owned apps
  316. * @param {number} steamID The ID to check
  317. * @returns {boolean} True when the game is owned
  318. */
  319. function isAppOwned(steamID) {
  320. if (steam_profile.rgOwnedApps.includes(parseInt(steamID))) {
  321. writeConsoleMessage('App: Owned - https://store.steampowered.com/app/' + steamID + '/')
  322. return true
  323. }
  324. // writeConsoleMessage('App: Unowned - https://store.steampowered.com/app/' + steamID + '/')
  325. return false
  326. }
  327.  
  328. /**
  329. * @description Checks the Steam game's ID against the wishlisted apps
  330. * @param {number} steamID The ID to check
  331. * @returns {boolean} True when the game is wishlisted
  332. */
  333. function isAppWishlisted(steamID) {
  334. if (steam_profile.rgWishlist.includes(parseInt(steamID))) {
  335. writeConsoleMessage('App: Wishlisted - https://store.steampowered.com/app/' + steamID + '/')
  336. return true
  337. }
  338. return false
  339. }
  340.  
  341. /**
  342. * @description Checks the Steam game's ID against the ignored apps
  343. * @param {number} steamID The ID to check
  344. * @returns {boolean} True when the game is ignored
  345. */
  346. function isAppIgnored(steamID) {
  347. if (typeof steam_profile.rgIgnoredApps[steamID] !== 'undefined') {
  348. writeConsoleMessage('App: Ignored - https://store.steampowered.com/app/' + steamID + '/')
  349. return true
  350. }
  351. return false
  352. }
  353.  
  354. /**
  355. * @description Parses a string for a Steam game ID
  356. * @param {string} str The string/URL that contains the Steam game ID
  357. * @returns {number} Steam game ID
  358. */
  359. function getSteamIDFromString(str) {
  360. let rgxId = /[0-9]{3,}/g
  361. let matches = str.match(rgxId)
  362. if (matches) {
  363. return parseInt(matches[0])
  364. }
  365. return null
  366. }
  367.  
  368. /**
  369. * Searches the document for Steam game ownership
  370. * @param {string|null} steamLinkSelector The CSS selector to match Steam links
  371. * @param {HTMLElement} elementToMark The element to mark as owned
  372. * @returns {undefined}
  373. */
  374. function markBySteamLinkSelector(steamLinkSelector, elementToMark) {
  375. let selectorQuery
  376. if (typeof steamLinkSelector === 'undefined' || steamLinkSelector === null) {
  377. selectorQuery = default_steam_url_selector
  378. } else {
  379. selectorQuery = steamLinkSelector
  380. }
  381.  
  382. document.querySelectorAll(selectorQuery).forEach(steamStoreLink => {
  383. let steamID = getSteamIDFromString(steamStoreLink.href)
  384. if (steamID !== null) {
  385. if (isAppOwned(steamID)) {
  386. if (typeof elementToMark === 'undefined' || elementToMark === null) {
  387. // No element passed, mark the link element itself
  388. setElementOwned(steamStoreLink)
  389. } else if (typeof elementToMark === 'function') {
  390. // Function passed, call the function passing in the link element
  391. let element = elementToMark(steamStoreLink)
  392. setElementOwned(element)
  393. } else {
  394. // Element passed, attempt to mark
  395. setElementOwned(elementToMark)
  396. }
  397. }
  398. }
  399. })
  400. }
  401.  
  402. /**
  403. * Checks a page for Steam game ownership
  404. * @param {string} storePageUrl The store page that contains the Steam link
  405. * @param {string|null} steamLinkSelector The CSS selector to match Steam links
  406. * @param {HTMLElement} elementToMark The element to mark as owned
  407. * @returns {undefined}
  408. */
  409. function markByStorePageUrl(storePageUrl, steamLinkSelector, elementToMark) {
  410. let selector
  411. if (typeof steamLinkSelector === 'undefined' || steamLinkSelector === null) {
  412. selector = default_steam_url_selector
  413. } else {
  414. selector = steamLinkSelector
  415. }
  416.  
  417. GM_xmlhttpRequest({
  418. method: 'GET',
  419. url: storePageUrl,
  420. onload: function (response) {
  421. let parser = new DOMParser()
  422. let storePage = parser.parseFromString(response.responseText, 'text/html')
  423.  
  424. let steamLink = storePage.querySelector(selector)
  425. if (steamLink !== null) {
  426. let steamID = getSteamIDFromString(steamLink.href)
  427. if (steamID !== null) {
  428. if (isAppOwned(steamID)) {
  429. setElementOwned(elementToMark)
  430. }
  431. }
  432. } else {
  433. writeConsoleMessage(`No steam links found on page "${storePageUrl}" with selector "${selector}"`)
  434. }
  435. }
  436. })
  437. return
  438. }
  439.  
  440. /**
  441. * Checks all matching links for Steam game ownership
  442. * @param {string} storePageSelector The CSS selector to match store links
  443. * @param {string} steamLinkSelector The CSS selector to match Steam links
  444. * @param {HTMLElement} elementToMark The element to mark as owned
  445. * @returns {undefined}
  446. */
  447. function markByStorePageSelector(storePageSelector, steamLinkSelector, elementToMark) {
  448. let storePageLinkElements = document.querySelectorAll(storePageSelector)
  449.  
  450. storePageLinkElements.forEach(storePageLinkElement => {
  451. if (typeof elementToMark === 'undefined' || elementToMark === null) {
  452. markByStorePageUrl(storePageLinkElement.href, steamLinkSelector, storePageLinkElement)
  453. } else if (typeof elementToMark === 'function') {
  454. // Function passed, call the function passing in the link element
  455. let element = elementToMark(storePageLinkElement)
  456. markByStorePageUrl(storePageLinkElement.href, steamLinkSelector, element)
  457. } else {
  458. writeConsoleMessage(storePageLinkElement)
  459. markByStorePageUrl(storePageLinkElement, steamLinkSelector, elementToMark)
  460. }
  461. })
  462. return
  463. }
  464.  
  465. async function GetSteamAppList() {
  466. return new Promise(function (resolve, reject) {
  467. setTimeout(function () {
  468. GM_xmlhttpRequest({
  469. method: 'GET',
  470. url: 'https://api.steampowered.com/ISteamApps/GetAppList/v2/',
  471. onload: response => {
  472. setTimeout(function () {
  473. if (response.status !== 200) {
  474. reject([
  475. response.status,
  476. response.statusText,
  477. response.readyState,
  478. response.responseHeaders,
  479. response.finalUrl
  480. ].join(', '))
  481. } else {
  482. let appList = JSON.parse(response.responseText).applist.apps
  483. appList.forEach(app => {
  484. app.name = app.name.toLowerCase().replace(/[^a-z0-9]/g, '')
  485. })
  486. resolve(appList)
  487. }
  488. }, 0)
  489. }
  490. })
  491. }, 0)
  492. })
  493. }
  494.  
  495. function main() {
  496. if (window !== window.parent) {
  497. // https://developer.mozilla.org/en-US/docs/Web/API/Window/parent
  498. // Don't run inside of a frame
  499. return
  500. }
  501.  
  502. if (!divButton) {
  503. divButton = document.createElement('div')
  504. }
  505. divButton.classList.add('bh_button')
  506. divButton.id = 'bh_markOwned'
  507.  
  508. let eleA = document.createElement('a')
  509. eleA.setAttribute('onclick', 'return false;')
  510. eleA.textContent = 'Mark Owned'
  511.  
  512. divButton.appendChild(eleA)
  513.  
  514. // Create button to refresh profile details
  515. // createCacheResetButton()
  516.  
  517. GM_addStyle(
  518. ' .bh_button { '
  519. + ' border-radius: 2px; border: medium none; padding: 10px; display: inline-block; '
  520. + ' cursor: pointer; background: #67C1F5 none repeat scroll 0% 0%; '
  521. + ' width: 120px; text-align: center; } '
  522. + ' .bh_button a { '
  523. + ' text-decoration: none !important; color: #FFF !important; '
  524. + ' padding: 0px 2px; } '
  525. + ' .bh_button:hover a { '
  526. + ' color: #0079BF !important; } '
  527. + ' .bh_button, .bh_button a { '
  528. + ' font-family: Verdana; font-size: 12px; '
  529. + ' line-height: 16px; } '
  530. + ' .bh_owned { background-color: #7CA156 !important; '
  531. + ' transition: background 500ms ease 0s; } '
  532. + ' #bh_markOwned { '
  533. + ' position: fixed; right: 20px; bottom: 20px; z-index: 33; } '
  534. + ' #bh_cacheReset { '
  535. + ' position: fixed; right: 20px; bottom: 60px; z-index: 33; } '
  536. + ' #bh_OpenLib { '
  537. + ' position: fixed; right: 20px; bottom: 65px; z-index: 33; } '
  538. )
  539.  
  540. let url = document.documentURI
  541.  
  542. if (url.includes('hrkgame.com')) {
  543. GM_addStyle(
  544. ' .bh_owned { background-color: #2B823A !important;/* background-color: #97BA22 !important;*/ } '
  545. + ' #bh_markOwned { bottom: 40px !important; } '
  546. + ' #bh_cacheReset { bottom: 80px !important; } '
  547. + '.catalog.ui.items .item.bh_owned .content a, .catalog.ui.items .item.bh_owned .content .description {color: #333 !important;}'
  548. + '.hrktable_content a.browse_catalogues_items div.label.bh_owned { background-color: #2B823A!important }'
  549. )
  550.  
  551. if (url.includes('/randomkeyshop/make-bundle')) {
  552. let onClickFunction = function () {
  553. document.querySelectorAll('#result div.header[data-href*=\'/games/product/\']').forEach(link => {
  554. markByStorePageUrl(link.getAttribute('data-href'), 'a.item[href*=\'store.steampowered.com/\']', element => element.parentElement.parentElement)
  555. })
  556. }
  557. addMarkBtnHandler(onClickFunction)
  558. } else if (url.includes('/games/products/?search')) {
  559. addMarkBtnHandler(markByStorePageSelector, ['.item a.header[href*=\'/games/product/\']', 'a.item[href*=\'store.steampowered.com/\']', element => element.parentElement.parentElement])
  560. } else if (url.includes('/games/product/')) {
  561. addMarkBtnHandler(markBySteamLinkSelector, ['a.item[href*=\'store.steampowered.com/\']', document.querySelector('.ui.maincontainer')])
  562. } else {
  563. let onClickFunction = function () {
  564. markByStorePageSelector('.offer_column a[href*=\'/games/product/\']', 'a.item[href*=\'store.steampowered.com/\']', element => element.parentElement)
  565.  
  566. document.querySelectorAll('a.browse_catalogues_items[href*=\'/games/product/\']').forEach(link => {
  567. if (link.textContent.includes('Steam')) {
  568. let elementToMark = link.querySelector('div.label')
  569. markByStorePageUrl(link.href, 'a.item[href*=\'store.steampowered.com/\']', elementToMark)
  570. }
  571. })
  572. }
  573. addMarkBtnHandler(onClickFunction)
  574. }
  575. } else if (url.includes('itch.io')) {
  576. if (url.includes('/my-collections') || url.includes('/my-purchases') || url.includes('/games') || url.includes('/s/') || url.includes('/c/') || url.includes('/b/')) {
  577. GM_addStyle(
  578. ' .grid_outer .game_grid_widget .game_cell.bh_owned span, .grid_outer .game_grid_widget .game_cell.bh_owned div, .grid_outer .game_grid_widget .game_cell.bh_owned a { color: #ffffff !important; } '
  579. )
  580.  
  581. addMarkBtnHandler(() => {
  582. let storePageLinkElements = document.querySelectorAll('.game_cell_data a.game_link')
  583.  
  584. storePageLinkElements.forEach(storePageLinkElement => {
  585. if (!storePageLinkElement.href.includes('/b/')) {
  586. // Don't search bundle pages for Steam links
  587. markByStorePageUrl(storePageLinkElement.href, default_steam_url_selector, storePageLinkElement.parentElement.parentElement.parentElement)
  588. }
  589. })
  590. })
  591. } else if (url.includes('/recommendations')) {
  592. GM_addStyle(
  593. ' .index_game_cell_widget.game_cell.bh_owned span, .index_game_cell_widget.game_cell.bh_owned div, .index_game_cell_widget.game_cell.bh_owned a.user_link { color: #ffffff; } '
  594. )
  595. addMarkBtnHandler(markByStorePageSelector, ['a.title', default_steam_url_selector, element => element.parentElement.parentElement])
  596. }
  597. } else if (url.includes('fanatical.com')) {
  598. GM_addStyle(
  599. ' .bh_owned { background-color: #0c6c22 !important; } '
  600. + ' .bh_owned .card-body div.card-body { background-color: #0c6c22 !important; } '
  601. + ' .bh_owned .card-body { background-color: #0c6c22; } '
  602. )
  603.  
  604. if (url.includes('/game/')) {
  605. addMarkBtnHandler(markBySteamLinkSelector, [default_steam_url_selector, () => document.querySelector('.details-content-container')])
  606. } else if (url.includes('/bundle/')) {
  607. let obTarget_root = document.querySelector('#root')
  608. if (obTarget_root) {
  609. let tmOb_root = -1
  610. let obMu_root = new MutationObserver(function (mutations) {
  611. mutations.forEach(function (mutation) {
  612. if (mutation.type !== 'attributes'
  613. || mutation.target.tagName === 'TR') {
  614. clearTimeout(tmOb_root)
  615. tmOb_root = setTimeoutCustom(function () {
  616. markBySteamLinkSelector(default_steam_url_selector, element => element.parentElement
  617. .parentElement.parentElement.parentElement)
  618. }, 200)
  619. }
  620. })
  621. })
  622.  
  623. let obConfig_root = { childList: true, subtree: true }
  624. obMu_root.observe(obTarget_root, obConfig_root)
  625. }
  626. } else if (url.includes('/pick-and-mix/')) {
  627. let onClickFunction = function () {
  628. let hook = __REACT_DEVTOOLS_GLOBAL_HOOK__
  629. let rootFragmentFiber = Array.from(hook.getFiberRoots(1))[0].current
  630. let rootCompFiber = rootFragmentFiber.child
  631. let rootComp = rootCompFiber.stateNode
  632. let state = rootComp.props.store.getState()
  633. let bundles = [...state.pickAndMix.all]
  634. bundles.forEach(bundle => {
  635. let productsArr = bundle.products
  636. let ownedGames = []
  637. productsArr.forEach(product => {
  638. let id = product._id
  639. if (isAppOwned(id)) {
  640. writeConsoleMessage('You own game ID: ' + id + ' - "' + product.name + '"')
  641. ownedGames.push(product.name)
  642. }
  643. })
  644. if (ownedGames.length > 0) {
  645. document.querySelectorAll('.card-overlay p').forEach(p => {
  646. if (ownedGames.includes(p.textContent)) {
  647. setElementOwned(p.parentElement.parentElement.parentElement.parentElement)
  648. }
  649. })
  650. }})
  651. }
  652. addMarkBtnHandler(onClickFunction)
  653. }
  654.  
  655. let onClickFunction = function () {
  656. let timeouts = []
  657. let gameUrls = []
  658. document.querySelectorAll('a[href*=\'/game/\']').forEach(game => {
  659. if (gameUrls.includes(game.href)) {
  660. return
  661. }
  662. gameUrls.push(game.href)
  663. timeouts.push(function () {
  664. let gamePage = game.href.replace('/en', '').replace('/game/', '/api/products/') + '/en'
  665. GM_xmlhttpRequest({
  666. method: 'GET',
  667. headers: {
  668. 'Host': 'www.fanatical.com',
  669. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0',
  670. 'Accept': 'application/json',
  671. 'Content-Type': 'application/x-www-form-urlencoded',
  672. 'Referer': game.href
  673. },
  674. url: gamePage,
  675. onload: function (response) {
  676. // writeConsoleMessage('status ' + response.status + ' ' + gamePage)
  677. if (response.status === 200) {
  678. let apiResponse = JSON.parse(response.responseText)
  679. if (typeof apiResponse.steam.id !== 'undefined') {
  680. if (isAppOwned(apiResponse.steam.id)) {
  681. setElementOwned(game.parentElement.parentElement.parentElement.parentElement)
  682. }
  683. }
  684. if (timeouts.length > 0) {
  685. setTimeout(timeouts.pop(), 50)
  686. }
  687. } else if (timeouts.length > 0) {
  688. setTimeout(timeouts.pop(), 400)
  689. }
  690. }
  691. })
  692. })
  693. })
  694. setTimeout(timeouts.pop(), 100)
  695. }
  696. addMarkBtnHandler(onClickFunction)
  697. } else if (url.includes('reddit.com')) {
  698. GM_addStyle(
  699. ' .bh_owned , .md .bh_owned code { background-color: #DFF0D8 !important; } '
  700. + ' li > .bh_owned, div > p > .bh_owned { padding: 0px 2px 0px 2px; } '
  701. )
  702.  
  703. addMarkBtnHandler(markOwned, ['td > a[href*=\'store.steampowered.com/\']', function (ele) {
  704. return ele.parentElement.parentElement
  705. }, null, 'bh_owned'])
  706.  
  707.  
  708. setTimeout(function () {
  709. markOwned('td > a[href*=\'store.steampowered.com/\']', function (ele) {
  710. return ele.parentElement.parentElement
  711. }, null, 'bh_owned')
  712.  
  713. markOwned('li > a[href*=\'store.steampowered.com/\']', function (ele) {
  714. return ele.parentElement
  715. }, null, 'bh_owned')
  716.  
  717. markOwned('li > p > a[href*=\'store.steampowered.com/\']', function (ele) {
  718. return ele.parentElement.parentElement
  719. }, null, 'bh_owned')
  720.  
  721. markOwned('div > p > a[href*=\'store.steampowered.com/\']'
  722. , null, null, 'bh_owned')
  723. }, 1000)
  724. } else if (url.includes('indiegala.com')) {
  725. GM_addStyle(
  726. ' #bh_markOwned {bottom: 70px !important;}'
  727. + ' .bh_owned, .bh_owned .bundle-item-trading { background-color: rgba(125, 174, 45, 0.9) !important; } '
  728. + ' .ig-bundle { padding-left: 3px; padding-right: 3px; margin-bottom: 3px; } '
  729. + ' .bh_owned.ig-bundle { background-color: rgba(125, 174, 45) !important; } '
  730. + ' .bh_owned.ig-bundle .bundle-item-trading { background-color: rgba(125, 174, 45, 0) !important; } '
  731. + ' .bh_owned .add-info-button-cont .left, .bh_owned .add-info-button-cont .palette-background-2 { '
  732. + ' background-color: #7DAE2D !important; } '
  733. + ' .bh_owned .add-info-button-cont .right .inner-info, .bh_owned .add-info-button-cont .right .palette-border-2 { '
  734. + ' border-color: #7DAE2D !important; } '
  735. + ' .bh_owned.medium-game .game-cover-medium { border: 3px solid #7DAE2D; background-color: rgba(125, 174, 45, 0.4); } '
  736. + ' .bh_owned.game-data-cont { background-color: #76AD1C !important; } '
  737. + ' .bundle-item-trading-cards-cont span { opacity: 0.7; } '
  738. + ' .span-title .title_game, .span-title .title_drm, .span-title .title_music { '
  739. + ' line-height: 43px !important; margin: 10px 0px 10px 15px !important; '
  740. + ' padding-left: 10px !important; border-radius: 3px !important; } '
  741. + ' .medium-game { min-height: 146px; } '
  742. )
  743.  
  744. // Insert email to bundle section
  745. let countRetryEmail = 10
  746. let tmRetryEmail = setInterval(function () {
  747. let eleEmail = document.querySelector('.account-email')
  748. let eleInput = document.querySelector('.email-input')
  749. if (eleEmail && eleInput) {
  750. let email = eleEmail.textContent.trim()
  751. if (email !== '') {
  752. eleInput.value = email
  753. clearInterval(tmRetryEmail)
  754. }
  755. }
  756.  
  757. if (countRetryEmail < 0) {
  758. clearInterval(tmRetryEmail)
  759. }
  760. countRetryEmail--
  761. }, 3000)
  762.  
  763. // Change title
  764. let countRetryTitle = 10
  765. let tmRetryTitle = setInterval(function () {
  766. let elesPrice = document.querySelectorAll('.bundle-claim-phrase')
  767. for (let i = elesPrice.length - 1; i > -1; i--) {
  768. let elePrice = elesPrice[i].querySelector('span')
  769. if (elePrice) {
  770. let price = elePrice.textContent.trim()
  771. if (price.indexOf('$') === 0) {
  772. document.title = price + ' ' + document.title
  773. clearInterval(tmRetryTitle)
  774. break
  775. }
  776. }
  777. }
  778.  
  779. if (countRetryTitle < 0) {
  780. clearInterval(tmRetryTitle)
  781. }
  782. countRetryTitle--
  783. }, 3000)
  784.  
  785. if (url.includes('indiegala.com/store/') || url.includes('indiegala.com/games') || url === 'https://www.indiegala.com/') {
  786. let onClickFunction = function () {
  787. let gameBrowserLinks = document.querySelectorAll('a.main-list-item-clicker')
  788. for (let i = 0; i < gameBrowserLinks.length; i++) {
  789. let steamID = getSteamIDFromString(gameBrowserLinks[i].href)
  790. if (steamID !== null) {
  791. if (isAppOwned(steamID)) {
  792. setElementOwned(gameBrowserLinks[i].parentElement)
  793. }
  794. }
  795. }
  796.  
  797. let smallListLinks = document.querySelectorAll('a.fit-click')
  798. for (let i = 0; i < smallListLinks.length; i++) {
  799. let steamID = getSteamIDFromString(smallListLinks[i].href)
  800. if (steamID !== null) {
  801. if (isAppOwned(steamID)) {
  802. setElementOwned(smallListLinks[i].parentElement.querySelector('.item-inner'))
  803. }
  804. }
  805. }
  806. }
  807. addMarkBtnHandler(onClickFunction)
  808. }
  809. } else if (url.includes('orlygift.com')) {
  810. addMarkBtnHandler(markByStorePageSelector, ['a[href*=\'/games/\']', default_steam_url_selector, element => element.parentElement])
  811. } else if (url.includes('cubicbundle.com')) {
  812. GM_addStyle(
  813. ' .bh_owned { background-color: #91BA07 !important; } '
  814. )
  815. addMarkBtnHandler(markOwned, ['.price a[href*=\'store.steampowered.com/\']', function (ele) {
  816. return ele.parentElement.parentElement.parentElement.parentElement.parentElement
  817. }, null, 'bh_owned'])
  818. } else if (url.includes('dailyindiegame.com')) {
  819. GM_addStyle(
  820. ' .bh_owned, .bh_owned a, .bh_owned a:not(:visited) .DIG2content { color: #202020 !important; } '
  821. )
  822.  
  823. let onClickFunction = function () {
  824. let markMap = [{
  825. selector: '.DIG-content a[href*=\'store.steampowered.com/\']',
  826. callback: function (ele) {
  827. return ele.parentElement
  828. .parentElement.parentElement
  829. .parentElement.parentElement
  830. }
  831. },
  832. {
  833. selector: '.DIG2content a[href*=\'store.steampowered.com/\']',
  834. callback: function (ele) {
  835. return ele.parentElement.parentElement
  836. }
  837. },
  838. {
  839. selector: '.DIG3_14_Gray a[href*=\'store.steampowered.com/\']',
  840. callback: function (ele) {
  841. return ele.parentElement.parentElement.parentElement
  842. }
  843. }]
  844. for (let i = 0; i < markMap.length; i++) {
  845. if (document.querySelectorAll(markMap[i].selector).length > 0) {
  846. markOwned(markMap[i].selector, markMap[i].callback, null, 'bh_owned')
  847. }
  848. }
  849. }
  850. addMarkBtnHandler(onClickFunction)
  851. } else if (url.includes('bundlekings.com')) {
  852. addMarkBtnHandler(markOwned, ['.content-wrap a[href*=\'store.steampowered.com/\']', function (ele) {
  853. return ele.parentElement.parentElement.parentElement
  854. }, null, 'bh_owned'])
  855. } else if (url.includes('otakumaker.com')) {
  856. GM_addStyle(
  857. ' .bh_owned { background-color: #91BA07 !important; } '
  858. )
  859. addMarkBtnHandler(markOwned, ['.gantry-width-spacer a[href*=\'store.steampowered.com/\']', function (ele) {
  860. return ele.parentElement.parentElement
  861. }, null, 'bh_owned'])
  862. } else if (url.includes('otakubundle.com')) {
  863. GM_addStyle(
  864. ' .bh_owned { background-color: #91BA07 !important; } '
  865. )
  866. addMarkBtnHandler(markOwned, ['#hikashop_product_left_part > .g-grid > .g-block > .g-block > a[href*=\'store.steampowered.com/\']',
  867. function (ele) {
  868. return ele.parentElement.parentElement
  869. },
  870. null,
  871. 'bh_owned'])
  872. } else if (url.includes('gogobundle.com')) {
  873. GM_addStyle(
  874. ' .bh_owned { background-color: #91BA07 !important; border: 1px solid white; } '
  875. )
  876.  
  877. addMarkBtnHandler(markOwned, ['.g-block > .g-block > a[href*=\'store.steampowered.com/\']', function (ele) {
  878. return ele.parentElement.parentElement
  879. }, null, 'bh_owned'])
  880. } else if (url.includes('superduperbundle.com')) {
  881. addMarkBtnHandler(markOwned, ['#gameslist a[href*=\'store.steampowered.com/\']', function (ele) {
  882. return ele.parentElement.parentElement
  883. }, null, 'bh_owned'])
  884. } else if (url.includes('gamebundle.com')) {
  885. GM_addStyle(
  886. ' .bh_owned { background-color: #A0CC41 !important; border-bottom: 45px solid rgba(233, 233, 233, 0.5); } '
  887. + ' .bh_owned .activebundle_game_bundle_debut_title { background-color: #A0CC41 !important; } '
  888. )
  889. addMarkBtnHandler(markOwned, ['.activebundle_game_section_full a[href*=\'store.steampowered.com/\']', function (ele) {
  890. return ele.parentElement
  891. }, null, 'bh_owned'])
  892. } else if (url.includes('humblebundle.com')) {
  893. GM_addStyle(
  894. ' .game-box img { max-height: 180px !important; max-width: 130px !important; } '
  895. + ' .image-grid { animation: none !important; } ' +
  896. ' .bh_owned .entity-details, .bh_owned div.entity-meta, .bh_owned div.entity-meta span.entity-title { background: #7CA156 !important; color: #FFFFFF !important; } ' +
  897. ' div.slick-track .bh_owned div.entity-meta { background: transparent !important; } '
  898. )
  899.  
  900. if (url.includes('/games/')) {
  901. GM_addStyle(
  902. ' .bh-owned div.dd-image-box-caption-container { background: #7CA156 !important; } ' +
  903. ' .bh-owned span.front-page-art-image-text { color: #FFFFFF !important; } '
  904. )
  905. let onClickFunction = function () {
  906. GetSteamAppList()
  907. .then(appList => {
  908. let gameTitleElements = document.querySelectorAll('span.front-page-art-image-text')
  909. for (let gameTitleElement of gameTitleElements) {
  910. let gameName = gameTitleElement.textContent.toLowerCase().replace(/[^a-z0-9]/g, '')
  911. let matches = appList.filter(app => app.name === gameName)
  912. for (let match of matches) {
  913. if (isAppOwned(match.appid)) {
  914. setElementOwned(gameTitleElement.parentElement.parentElement.parentElement.parentElement)
  915. break
  916. }
  917. }
  918. }
  919. })
  920. }
  921. addMarkBtnHandler(onClickFunction)
  922. } else if (url.includes('/store')) {
  923. let onClickFunction = function () {
  924. GetSteamAppList()
  925. .then(appList => {
  926. let gameTitleElements = document.querySelectorAll('span.entity-title')
  927. for (let gameTitleElement of gameTitleElements) {
  928. let gameName = gameTitleElement.textContent.toLowerCase().replace(/[^a-z0-9]/g, '')
  929. let matches = appList.filter(app => app.name === gameName)
  930. for (let match of matches) {
  931. if (isAppOwned(match.appid)) {
  932. setElementOwned(gameTitleElement.parentElement.parentElement)
  933. break
  934. }
  935. }
  936. }
  937. })
  938. }
  939. addMarkBtnHandler(onClickFunction)
  940. } else if (url.includes('/subscription')) {
  941. GM_addStyle(
  942. ' .bh-owned div { background: #7CA156 !important; } ' +
  943. ' .bh-owned .content-choice-title { color: #FFFFFF !important; } '
  944. )
  945. let onClickFunction = function () {
  946. GetSteamAppList()
  947. .then(appList => {
  948. let gameTitleElements = document.querySelectorAll('span.content-choice-title')
  949. for (let gameTitleElement of gameTitleElements) {
  950. let gameName = gameTitleElement.textContent.toLowerCase().replace(/[^a-z0-9]/g, '')
  951. let matches = appList.filter(app => app.name === gameName)
  952. for (let match of matches) {
  953. if (isAppOwned(match.appid)) {
  954. setElementOwned(gameTitleElement.parentElement.parentElement)
  955. break
  956. }
  957. }
  958. }
  959. })
  960. }
  961. addMarkBtnHandler(onClickFunction)
  962. }
  963. } else if (url.includes('steamcompanion.com')) {
  964. GM_addStyle(
  965. ' .bh_owned.banner { margin-bottom: 5px !important; margin-top: 35px !important; '
  966. + ' padding-bottom: 15px !important; padding-top: 15px !important; } '
  967. + ' .bh_owned.giveaway-links { opacity: 0.75; } '
  968. )
  969.  
  970. markOwned('#hero a[href*=\'store.steampowered.com/\']'
  971. , null, null, 'bh_owned')
  972.  
  973. // Mark
  974. {
  975. let query = '.giveaway-links img[src^=\'https://steamcdn-a.akamaihd.net/steam/apps/\']'
  976. let getLabelCallback = function (ele) {
  977. return ele.parentElement.parentElement.parentElement
  978. }
  979.  
  980. let apps = []
  981.  
  982. let eleApps = document.querySelectorAll(query)
  983.  
  984. for (let i = 0; i < eleApps.length; i++) {
  985. let app = /[0-9]+/.exec(eleApps[i].getAttribute('src'))
  986. if (app !== null) {
  987. apps.push(app[0])
  988. }
  989. }
  990.  
  991. apps = apps.filter(function (elem, index, self) {
  992. return index === self.indexOf(elem)
  993. })
  994.  
  995. writeConsoleMessage('Apps: ' + apps.length)
  996. let appAll = apps.join(',')
  997.  
  998. GM_xmlhttpRequest(
  999. {
  1000. method: 'GET',
  1001. headers:
  1002. {
  1003. 'Cache-Control': 'max-age=0'
  1004. },
  1005. url: 'https://store.steampowered.com/api/appuserdetails/?appids=' + appAll,
  1006. onload: function (response) {
  1007. let dataRes = JSON.parse(response.responseText)
  1008.  
  1009. let countOwned = 0
  1010.  
  1011. let elementApps = document.querySelectorAll(query)
  1012. for (let i = 0; i < elementApps.length; i++) {
  1013. let appUrl = elementApps[i].getAttribute('src')
  1014. if (appurl.includes('https://steamcdn-a.akamaihd.net/steam/apps/')) {
  1015. let app = /[0-9]+/.exec(appUrl)
  1016. if (app !== null) {
  1017. if (typeof dataRes[app] !== 'undefined') {
  1018. if (dataRes[app].success) {
  1019. if (dataRes[app].data.is_owned) {
  1020. let eleLabel = getLabelCallback(elementApps[i])
  1021. eleLabel.classList.add('bh_owned')
  1022. countOwned++
  1023. } else {
  1024. // writeConsoleMessage("App: not owned - http://store.steampowered.com/app/" + app + "/");
  1025. }
  1026. } else {
  1027. // writeConsoleMessage("App: not success - https://steamdb.info/app/" + app + "/");
  1028. }
  1029. }
  1030. }
  1031. }
  1032. }
  1033.  
  1034. writeConsoleMessage('Apps: owned - ' + countOwned)
  1035. }
  1036. // End onload
  1037. })
  1038. }
  1039. } else if (url.includes('store.steampowered.com')) {
  1040. if (url.includes('/widget/')) {
  1041. GM_addStyle(
  1042. ' .bh_owned { background-color: transparent !important; } '
  1043. + ' .bh_owned a { color: #71A034 !important; }'
  1044. )
  1045.  
  1046. markOwned('.main_text a[href*=\'store.steampowered.com/\']', function (ele) {
  1047. return ele.parentElement
  1048. }, null, 'bh_owned')
  1049. } else if (url.includes('/app/')) {
  1050. GM_addStyle(
  1051. ' .bh_owned { '
  1052. + ' background-color: #6D8C1A !important; '
  1053. + ' padding: 0px 2px 0px 2px; '
  1054. + ' } '
  1055. )
  1056.  
  1057. markOwned(
  1058. '.glance_details p > a[href*=\'store.steampowered.com/\']'
  1059. + ', .game_area_dlc_bubble a[href*=\'store.steampowered.com/\']'
  1060. ,
  1061. null,
  1062. null,
  1063. 'bh_owned')
  1064. } else if (url.includes('/notinterested/')) {
  1065. GM_addStyle(
  1066. ' .bh_owned { '
  1067. + ' background-color: #6D8C1A !important; '
  1068. + ' padding: 5px 100px 5px 5px !important; '
  1069. + ' margin-left: -5px; margin-right: 50px; '
  1070. + ' } '
  1071. )
  1072.  
  1073. markOwned('.ignoredapps > a[href*=\'store.steampowered.com/\']'
  1074. , null, null, 'bh_owned')
  1075. } else if (url.includes('/search/')) {
  1076. GM_addStyle(
  1077. ' .bh_owned { '
  1078. + ' background-color: #6D8C1A66 !important; '
  1079. + ' } '
  1080. )
  1081.  
  1082. markOwned('.search_result_row[href*=\'store.steampowered.com/\']'
  1083. , null, null, 'bh_owned')
  1084. }
  1085. } else if (url.includes('steamcommunity.com')) {
  1086. GM_addStyle(
  1087. ' .bh_owned { background-color: #71A034 !important; '
  1088. + ' padding: 0px 2px 0px 2px; } '
  1089. + ' .bh_owned.blotter_userstatus_game { padding: 0px; border-color: #71A034; } '
  1090. )
  1091.  
  1092. if (url.includes('/home')) {
  1093. let querySteamHome = '.blotter_gamepurchase_details a[href*=\'store.steampowered.com/\']:not(.bh_owned) '
  1094. + ', .blotter_author_block a[href*=\'store.steampowered.com/\']:not(.bh_owned) '
  1095. + ', .blotter_author_block a[href*=\'steamcommunity.com/app/\']:not(.bh_owned) '
  1096. + ', .blotter_daily_rollup_line a[href*=\'steamcommunity.com/app/\']:not(.bh_owned) '
  1097. markOwned(querySteamHome, function (ele, type) {
  1098. if (type === 1) {
  1099. if (ele.classList.contains('blotter_userstats_game')) {
  1100. ele.parentElement.classList.add('bh_owned')
  1101. } else {
  1102. ele.classList.add('bh_owned')
  1103. }
  1104. }
  1105. })
  1106.  
  1107. let targetObMark = document.getElementById('blotter_content')
  1108. if (targetObMark) {
  1109. let tmObMark = -1
  1110. let obMark = new MutationObserver(function (mutations) {
  1111. mutations.forEach(function () {
  1112. clearTimeout(tmObMark)
  1113. tmObMark = setTimeout(function (querySteamH) {
  1114. markOwned(querySteamH, function (ele, type) {
  1115. if (type === 1 && !ele.classList.contains('blotter_userstats_game')) {
  1116. ele.classList.add('bh_owned')
  1117. }
  1118. })
  1119. }, 100, querySteamHome)
  1120. })
  1121. })
  1122.  
  1123. let configObMark = { childList: true }
  1124. obMark.observe(targetObMark, configObMark)
  1125. }
  1126. } else if (url.includes('/announcements')) {
  1127. markOwned('.announcement_body a[href*=\'store.steampowered.com/\']'
  1128. , null, null, 'bh_owned')
  1129. }
  1130. } else if (url.includes('forums.steampowered.com')) {
  1131. GM_addStyle(
  1132. ' .bh_owned { background-color: #71A034 !important; '
  1133. + ' padding: 0px 2px 0px 2px;'
  1134. + ' } '
  1135. )
  1136.  
  1137. markOwned('div[id^=\'post_message\'] a[href*=\'store.steampowered.com/\']'
  1138. , null, null, 'bh_owned')
  1139. } else if (url.includes('whosgamingnow.net')) {
  1140. if (url.includes('/discussion')) {
  1141. GM_addStyle(
  1142. ' .bh_owned { '
  1143. + ' padding: 0px 2px 0px 2px;'
  1144. + ' } '
  1145. )
  1146.  
  1147. markOwned('.MessageList a[href*=\'store.steampowered.com/\']'
  1148. , null, null, 'bh_owned')
  1149. } else if (url.includes('/redeem')) {
  1150. GM_addStyle(
  1151. ' .bh_owned { '
  1152. + ' border: 1px solid #FFF;'
  1153. + ' } '
  1154. + ' .bh_owned .BoxArt { '
  1155. + ' border: 0px !important;'
  1156. + ' } '
  1157. )
  1158.  
  1159. markOwned('.GameInfo a[href*=\'store.steampowered.com/\']', function (ele) {
  1160. return ele.parentElement.parentElement.parentElement
  1161. })
  1162. } else if (url.includes('/giveaway')) {
  1163. GM_addStyle(
  1164. ' .bh_owned { '
  1165. + ' border: 5px solid #7CA156;'
  1166. + ' } '
  1167. )
  1168.  
  1169. markOwned('img[src*=\'://cdn.akamai.steamstatic.com/steam/\']'
  1170. , null, null, 'bh_owned')
  1171. }
  1172. } else if (url.includes('steamground.com') && url.includes('/wholesale')) {
  1173. GM_addStyle(
  1174. ' .bh_owned { background-color: #48B24B !important; } '
  1175. + ' .bh_owned .wholesale-card_title { color: #373d41 !important; } '
  1176. + ' .bh_steam { display: none; } '
  1177. )
  1178.  
  1179. let elesTitle = document.querySelectorAll('.wholesale-card_title')
  1180. if (elesTitle.length > 0) {
  1181. GM_xmlhttpRequest(
  1182. {
  1183. method: 'GET',
  1184. url: 'https://www.steamgifts.com/discussion/iy081/steamground-wholesale-build-a-bundle',
  1185. onload: function (response) {
  1186. let data = response.responseText
  1187. let eleContainer = document.createElement('div')
  1188. eleContainer.innerHTML = data
  1189.  
  1190. let eleComment = eleContainer.querySelector('.comment__description')
  1191. if (eleComment) {
  1192. let elesGame = eleComment.querySelectorAll('table td:nth-child(1) a[href*=\'store.steampowered.com/\']')
  1193. if (elesGame.length > 0) {
  1194. let arrTitle = []
  1195. for (let i = 0; i < elesTitle.length; i++) {
  1196. arrTitle.push(elesTitle[i].textContent.trim())
  1197. }
  1198.  
  1199. for (let i = 0; i < elesGame.length; i++) {
  1200. let isMatch = false
  1201. let game = elesGame[i].textContent.trim().toLowerCase()
  1202. for (let j = 0; j < elesTitle.length; j++) {
  1203. let title = elesTitle[j].textContent.trim().toLowerCase()
  1204. if (game === title
  1205. || (title.indexOf('|') > -1 && game === title.replace('|', ':'))
  1206. || (game === 'ball of light' && title === 'ball of light (journey)')
  1207. || (game === 'its your last chance in new school' && title === 'it is yоur last chance in new schооl')
  1208. || (game === 'shake your money simulator 2016' && title === 'shake your money simulator')
  1209. || (game === 'spakoyno: back to the ussr 2.0' && title === 'spakoyno back to the ussr 2.0')
  1210. || (game === 'or' && title === 'or!')) {
  1211. isMatch = true
  1212.  
  1213. arrTitle = arrTitle.filter(function (value) {
  1214. return value !== elesTitle[j].textContent.trim()
  1215. })
  1216. }
  1217.  
  1218. if (isMatch) {
  1219. let elemA = document.createElement('a')
  1220. elemA.classList.add('bh_steam')
  1221. elemA.href = elesGame[i].href
  1222. elesTitle[j].parentElement.parentElement.appendChild(elemA)
  1223.  
  1224. break
  1225. }
  1226. }
  1227. if (!isMatch) {
  1228. writeConsoleMessage('Not match: ' + elesGame[i].href + ' ' + elesGame[i].textContent)
  1229. }
  1230. }
  1231.  
  1232. if (arrTitle.length > 0) {
  1233. writeConsoleMessage('Not match: ' + arrTitle.length)
  1234. for (let i = 0; i < arrTitle.length; i++) {
  1235. writeConsoleMessage('Not match: ' + arrTitle[i])
  1236. }
  1237. }
  1238.  
  1239. markOwned('.wholesale-card > a[href*=\'store.steampowered.com/\']', function (ele) {
  1240. return ele.parentElement
  1241. }, null, 'bh_owned')
  1242. }
  1243. }
  1244. }
  1245. // End onload
  1246. })
  1247. }
  1248. } else if (url.includes('bunchkeys.com')) {
  1249. GM_addStyle(
  1250. ' .bh_owned { border: #B5D12E 3px solid !important; '
  1251. + ' margin-left: -3px; margin-top: -3px; } '
  1252. )
  1253.  
  1254. addMarkBtnHandler(markOwned, [default_steam_url_selector, function (ele) {
  1255. return ele.parentElement
  1256. }, null, 'bh_owned'])
  1257. } else if (url.includes('sgtools.info')) {
  1258. GM_addStyle(
  1259. ' .bh_owned { background-color: #71A034 !important; } '
  1260. )
  1261. if (url.includes('/lastbundled')) {
  1262. markOwned('#content > div > table > tbody > tr > td > a[href*=\'store.steampowered.com/\']', function (ele) {
  1263. return ele.parentElement.parentElement
  1264. }, null, 'bh_owned')
  1265. } else if (url.includes('/deals')) {
  1266. markOwned('.deal_game_image > img[src*=\'cdn.akamai.steamstatic.com/steam/\']', function (ele) {
  1267. return ele.parentElement
  1268. }, null, 'bh_owned')
  1269. } else if (url.includes('/whitelisted')) {
  1270. markOwned('.cmGame > a[href*=\'store.steampowered.com/\']', function (ele) {
  1271. return ele.parentElement
  1272. }, null, 'bh_owned')
  1273. }
  1274. } else if (url.includes('steamkeys.ovh')) {
  1275. markOwned('td > a[href*=\'store.steampowered.com/\']', function (ele) {
  1276. return ele.parentElement.parentElement
  1277. }, null, 'bh_owned')
  1278. } else if (url.includes('steamdb.info')) {
  1279. if (window !== window.parent) {
  1280. return
  1281. }
  1282.  
  1283. GM_addStyle(
  1284. ' .bh_owned, tr.bh_owned td { background-color: #DDF7D3 !important; } '
  1285. + ' .bh_owned_transparent { background-color: #bcf0a880 !important; } '
  1286. )
  1287.  
  1288. markOwned(' \
  1289. #apps .app \
  1290. , #dlc .app \
  1291. , .container > .table .app \
  1292. , .sales-section .app \
  1293. , .page-search .app \
  1294. ', null, function (ele) {
  1295. return ele.getAttribute('data-appid')
  1296. }, 'bh_owned')
  1297.  
  1298. markOwned(' \
  1299. #subs .package \
  1300. , .sales-section .package \
  1301. , .page-search .package \
  1302. ', null, function (ele) {
  1303. return '/steam/subs/' + ele.getAttribute('data-subid')
  1304. }, 'bh_owned')
  1305.  
  1306. markOwned('.table-products .app'
  1307. , null, function (ele) {
  1308. return ele.getAttribute('data-appid')
  1309. }, 'bh_owned_transparent')
  1310.  
  1311. markOwned('.app-history .appid'
  1312. , function (ele) {
  1313. return ele.parentElement
  1314. }, function (ele) {
  1315. return ele.textContent.trim()
  1316. }, 'bh_owned')
  1317. }
  1318.  
  1319. window.addEventListener('beforeunload', function () {
  1320. clearTimeoutAll()
  1321. clearIntervalAll()
  1322. })
  1323. }
  1324.  
  1325. attachOnReady(main)
  1326. }())

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址