Greasy Fork 还支持 简体中文。

Fanatical Keys Backup

Displays a text area with game titles and keys so you can copy them out easily.

  1. // ==UserScript==
  2. // @name Fanatical Keys Backup
  3. // @namespace Lex@GreasyFork
  4. // @version 0.3.0
  5. // @description Displays a text area with game titles and keys so you can copy them out easily.
  6. // @author Lex
  7. // @match https://www.fanatical.com/en/orders*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // Formats games array to a string to be displayed
  15. // Games is an array [ [title, key], ... ]
  16. function formatGames(games, includeUnrevealed, bundleTitle) {
  17. if (!includeUnrevealed)
  18. games = games.filter(e => e.gameKey);
  19. // Format the output as tab-separated
  20. if (bundleTitle) {
  21. games = games.map(e => bundleTitle + "\t" + e.gameTitle + "\t" + e.gameKey);
  22. } else {
  23. games = games.map(e => e.gameTitle + "\t" + e.gameKey);
  24. }
  25. return games.join("\n");
  26. }
  27.  
  28. function revealAllKeys(articles) {
  29. articles.filter(a => !a.gameKey).forEach(a => {
  30. a.element.querySelector(".key-container button").click();
  31. });
  32. }
  33.  
  34. function createRevealButton(bundle) {
  35. const btn = document.createElement("button");
  36. btn.type = "button"; // no default behavior
  37. btn.innerText = "Reveal this bundle's keys";
  38. btn.addEventListener("click", () => {
  39. revealAllKeys(bundle.articles);
  40. btn.style.display = "none";
  41. })
  42. return btn;
  43. }
  44.  
  45. function createCopyButton(area) {
  46. const btn = document.createElement("button");
  47. btn.type = "button";
  48. btn.textContent = "Copy to Clipboard";
  49. btn.style.cssText = "display: block; margin: 5px 0; padding: 5px 10px; cursor: pointer;";
  50. btn.addEventListener("click", async () => {
  51. await navigator.clipboard.writeText(area.value);
  52. btn.textContent = "Copied!";
  53. setTimeout(() => (btn.textContent = "Copy to Clipboard"), 1500);
  54. });
  55. return btn;
  56. }
  57.  
  58. function createConfig(updateCallback) {
  59. const createCheckbox = (labelText, className, defaultChecked) => {
  60. const label = document.createElement("label");
  61. label.style.marginRight = "10px";
  62.  
  63. const checkbox = document.createElement("input");
  64. checkbox.type = "checkbox";
  65. checkbox.className = className;
  66. checkbox.checked = defaultChecked;
  67. checkbox.addEventListener("change", updateCallback);
  68.  
  69. label.append(` ${labelText} `, checkbox,);
  70. return label;
  71. };
  72.  
  73. const container = document.createElement("div");
  74. container.append(
  75. createCheckbox("Include Bundle Title", "includeTitle", false),
  76. createCheckbox("Include Unrevealed", "includeUnrevealed", false)
  77. );
  78. container.className = "ktt-config-container"
  79. return container;
  80. }
  81.  
  82. // Adds a textarea to the bottom of the games listing with all the titles and keys
  83. function handleBundle(bundle) {
  84. const games = bundle.articles;
  85. const keyCount = games.filter(e => e.gameKey).length;
  86.  
  87. const lastArticleElement = bundle.articles[bundle.articles.length - 1].element;
  88. let div = lastArticleElement.nextElementSibling;
  89. if (!div || div.className !== "ktt-output-container") {
  90. div = document.createElement("div")
  91. div.className = "ktt-output-container"
  92. div.style.width = "100%";
  93. lastArticleElement.insertAdjacentElement('afterend', div);
  94.  
  95. if (games.length != keyCount) {
  96. div.append(createRevealButton(bundle));
  97. }
  98.  
  99. const notify = document.createElement("div");
  100. notify.className = "ktt-notify";
  101.  
  102. const configCallback = () => { refreshOutput(); };
  103.  
  104. const area = document.createElement("textarea");
  105. area.className = "ktt-area";
  106. area.style.width = "100%";
  107. area.setAttribute('readonly', true);
  108. div.append(notify, createConfig(configCallback), area, createCopyButton(area));
  109. }
  110.  
  111. const color = games.length === keyCount ? "" : "tomato";
  112. let newInner = `Dumping keys for ${bundle.name}: Found ${games.length} items and <span style="background-color:${color}">${keyCount} keys</span>.`;
  113. if (games.length != keyCount) {
  114. newInner += " Are some keys not revealed?";
  115. }
  116. const notify = div.querySelector(".ktt-notify");
  117. if (notify.innerHTML != newInner) {
  118. notify.innerHTML = newInner;
  119. }
  120.  
  121. const area = div.querySelector(".ktt-area");
  122. const includeTitle = div.querySelector(".includeTitle").checked;
  123. const includeUnrevealed = div.querySelector(".includeUnrevealed").checked;
  124. const gameStr = formatGames(games, includeUnrevealed, includeTitle ? bundle.name : "");
  125. if (area.value != gameStr) {
  126. area.value = gameStr;
  127. // Adjust the height so all the contents are visible
  128. area.style.height = "";
  129. area.style.height = area.scrollHeight + 20 + "px";
  130. }
  131. }
  132.  
  133. function refreshOutput() {
  134. let currentBundle = null;
  135. const bundles = [];
  136.  
  137. function traverse(element) {
  138. if (!element) return;
  139. if (element.matches("section")) {
  140. const bundleContainer = element.querySelector(".bundle-name-container");
  141. if (bundleContainer) {
  142. const bundleTitle = bundleContainer.textContent.trim();
  143. if (currentBundle && currentBundle.articles.length === 0) {
  144. currentBundle.name = bundleTitle;
  145. } else {
  146. currentBundle = {
  147. name: bundleTitle,
  148. articles: []
  149. };
  150. bundles.push(currentBundle);
  151. }
  152. }
  153. }
  154.  
  155. if (element.matches("article")) {
  156. if (!currentBundle) {
  157. currentBundle = {
  158. name: "unknown",
  159. articles: []
  160. }
  161. bundles.push(currentBundle)
  162. }
  163. currentBundle.articles.push({
  164. element,
  165. gameTitle: element.querySelector(".game-name")?.textContent.trim() ?? "",
  166. gameKey: element.querySelector("[aria-label='reveal-key']")?.value ?? "",
  167. });
  168. return; // Stop traversing further inside this article
  169. }
  170.  
  171. for (const child of element.children) {
  172. if (child)
  173. traverse(child);
  174. }
  175. }
  176. const container = document.querySelector("section.single-order");
  177. traverse(container);
  178.  
  179. bundles.forEach(handleBundle);
  180.  
  181. return bundles;
  182. }
  183.  
  184. let loopCount = 0;
  185. function handleOrderPage() {
  186. const bundles = refreshOutput();
  187.  
  188. if (bundles.length > 0) {
  189. if (loopCount++ < 100) {
  190. setTimeout(handleOrderPage, 500);
  191. }
  192. } else {
  193. if (loopCount++ < 100) {
  194. setTimeout(handleOrderPage, 100);
  195. }
  196. }
  197. }
  198.  
  199. handleOrderPage();
  200. })();