Itch.io Web Integration

Shows if an Itch.io link has been claimed or not

2020-06-06 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name Itch.io Web Integration
  3. // @namespace Lex@GreasyFork
  4. // @match *://*/*
  5. // @grant GM_xmlhttpRequest
  6. // @grant GM_getValue
  7. // @grant GM_setValue
  8. // @version 0.1.3
  9. // @author Lex
  10. // @description Shows if an Itch.io link has been claimed or not
  11. // @connect itch.io
  12. // ==/UserScript==
  13.  
  14. // TODO
  15. // Add ability to claim the game with one click
  16.  
  17. (function(){
  18. 'use strict';
  19. const INVALIDATION_TIME = 5*60*60*1000; // 5 hour cache time
  20. const ITCH_GAME_CACHE_KEY = 'ItchGameCache';
  21. var ItchGameCache;
  22. // Promise wrapper for GM_xmlhttpRequest
  23. const Request = details => new Promise((resolve, reject) => {
  24. details.onerror = details.ontimeout = reject;
  25. details.onload = resolve;
  26. GM_xmlhttpRequest(details);
  27. });
  28. function loadItchCache() {
  29. ItchGameCache = JSON.parse(GM_getValue(ITCH_GAME_CACHE_KEY) || '{}');
  30. }
  31. function _saveItchCache() {
  32. if (ItchGameCache === undefined) return;
  33. GM_setValue(ITCH_GAME_CACHE_KEY, JSON.stringify(ItchGameCache));
  34. }
  35. function setItchGameCache(key, game) {
  36. loadItchCache(); // refresh our cache in case another tab has edited it
  37. ItchGameCache[key] = game;
  38. _saveItchCache();
  39. }
  40. function deleteItchGameCache(key) {
  41. if (key === undefined) return;
  42. loadItchCache();
  43. delete ItchGameCache[key];
  44. _saveItchCache();
  45. }
  46. function getItchGameCache(link) {
  47. if (!ItchGameCache) loadItchCache();
  48. if (Object.prototype.hasOwnProperty.call(ItchGameCache, link)) {
  49. return ItchGameCache[link];
  50. }
  51. return null;
  52. }
  53. // Sends an XHR request and parses the results into a game object
  54. async function fetchItchGame(url) {
  55. const response = await Request({method: "GET",
  56. url: url});
  57. const parser = new DOMParser();
  58. const dom = parser.parseFromString(response.responseText, 'text/html');
  59. // Gets the inner text of an element if it can be found otherwise returns undefined
  60. const txt = query => { const e = dom.querySelector(query); return e && e.innerText.trim(); };
  61. const game = {};
  62. game.cachetime = (new Date()).getTime();
  63. game.url = url;
  64. const h2 = dom.querySelector(".purchase_banner_inner .key_row .ownership_reason");
  65. game.isOwned = h2 !== null;
  66. game.isClaimable = [...dom.querySelectorAll(".buy_btn")].filter(e => e.innerText == "Download or claim").length > 0;
  67. game.isFree = [...dom.querySelectorAll("span[itemprop=price]")].filter(e => e.innerText == "$0.00 USD").length > 0;
  68. game.original_price = txt("span.original_price");
  69. game.price = txt("span[itemprop=price]");
  70. game.saleRate = txt(".sale_rate");
  71. return game;
  72. }
  73. // Loads an itch game from cache or fetches the page if needed
  74. async function getItchGame(url) {
  75. let game = getItchGameCache(url);
  76. if (game !== null) {
  77. const isExpired = (new Date()).getTime() - game.cachetime > INVALIDATION_TIME;
  78. if (isExpired) {
  79. game = null;
  80. }
  81. }
  82. if (game === null) {
  83. game = await fetchItchGame(url);
  84. setItchGameCache(url, game);
  85. }
  86. return game;
  87. }
  88. // Appends the isOwned tag to an anchor link
  89. function appendTags(a, game) {
  90. const div = document.createElement("div");
  91. a.after(div);
  92. let ownMark = '';
  93. if (game.isOwned) {
  94. ownMark = `<span title="Game is already claimed on itch.io">✔️</span>`;
  95. } else {
  96. if (!game.isClaimable) {
  97. let tooltip = game.price ? `🛒 Game costs ${game.price}` : `Game is not free`;
  98. ownMark = `<span title="${tooltip}">⛔</span>`;
  99. } else {
  100. const origPrice = game.original_price ? ` 🛒 Original price: ${game.original_price} 💸 Current Price: ${game.price}` : '';
  101. ownMark = `<span title="Game is claimable but you haven't claimed it.${origPrice}">❌</span>`;
  102. }
  103. }
  104. div.outerHTML = `<div style="margin-left: 5px; background:rgb(200,200,200); border-radius: 5px; display: inline-block;">${ownMark}</div>`;
  105. }
  106. function addClickHandler(a) {
  107. a.addEventListener('mouseup', event => {
  108. deleteItchGameCache(event.target.href);
  109. });
  110. }
  111.  
  112. // Handles an itch.io link on a page
  113. async function handleLink(a) {
  114. addClickHandler(a);
  115. const game = await getItchGame(a.href);
  116. appendTags(a, game);
  117. }
  118. // Finds all the itch.io links on the current page
  119. function getItchLinks() {
  120. let links = [...document.querySelectorAll("a[href*='itch.io/']")];
  121. links = links.filter(a => /^https:\/\/[^.]+\.itch\.io\/[^/]+$/.test(a.href));
  122. links = links.filter(a => !a.classList.contains("return_link"));
  123. links = links.filter(a => { const t = a.textContent.trim(); return t !== "" && t !== "GIF"; });
  124. return links;
  125. }
  126. function handlePage() {
  127. const as = getItchLinks();
  128. as.forEach(handleLink);
  129. }
  130. handlePage();
  131. })();