Humble Bundle Keys Backup

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

  1. // ==UserScript==
  2. // @name Humble Bundle Keys Backup
  3. // @namespace Lex@GreasyFork
  4. // @version 0.2.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.humblebundle.com/downloads*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. function formatGames(games, bundleTitle) {
  15. // Format the output as tab-separated
  16. if (bundleTitle) {
  17. games = games.map(e => (bundleTitle + "\t" + e.title+"\t"+e.key).trim());
  18. } else{
  19. games = games.map(e => (e.title+"\t"+e.key).trim());
  20. }
  21. return games.join("\n");
  22. }
  23.  
  24. function createNotify() {
  25. const notify = document.createElement("div");
  26. notify.className = "ktt-notify";
  27. return notify;
  28. }
  29.  
  30. function updateNotify(bundle, message) {
  31. const notify = bundle.querySelector(".ktt-notify");
  32. notify.innerHTML = message;
  33. }
  34.  
  35. function createConfig(updateCallback) {
  36. const createCheckbox = (labelText, className, defaultChecked) => {
  37. const label = document.createElement("label");
  38. label.style.marginRight = "10px";
  39.  
  40. const checkbox = document.createElement("input");
  41. checkbox.type = "checkbox";
  42. checkbox.className = className;
  43. checkbox.checked = defaultChecked;
  44. checkbox.addEventListener("change", updateCallback);
  45.  
  46. label.append(` ${labelText} `, checkbox,);
  47. return label;
  48. };
  49.  
  50. const container = document.createElement("div");
  51. container.append(
  52. createCheckbox("Include Bundle Title", "includeTitle", false),
  53. createCheckbox("Include Unrevealed", "includeUnrevealed", true)
  54. );
  55. container.className = "ktt-config-container"
  56. return container;
  57. }
  58.  
  59. function createArea() {
  60. const area = document.createElement("textarea");
  61. area.className = "key-text-area";
  62. area.style.width = "100%";
  63. area.setAttribute('readonly', true);
  64. return area;
  65. }
  66.  
  67. // Updates an area if it needs updating, adjusting the height to fit the contents
  68. function updateArea(bundle, updateStr) {
  69. const area = bundle.querySelector(".key-text-area")
  70. if (area.value != updateStr) {
  71. area.value = updateStr;
  72. // Adjust the height so all the contents are visible
  73. area.style.height = "";
  74. area.style.height = area.scrollHeight + 20 + "px";
  75. }
  76. }
  77.  
  78. function createCopyButton(area) {
  79. const button = document.createElement("button");
  80. button.textContent = "Copy to Clipboard";
  81. button.style.cssText = "display: block; margin: 5px 0; padding: 5px 10px; cursor: pointer;";
  82. button.addEventListener("click", async () => {
  83. await navigator.clipboard.writeText(area.value);
  84. button.textContent = "Copied!";
  85. setTimeout(() => (button.textContent = "Copy to Clipboard"), 1500);
  86. });
  87. return button;
  88. }
  89.  
  90. // Returns array of the games in the target bundle
  91. function getGames(bundle) {
  92. let games = [];
  93. bundle.querySelectorAll(".key-redeemer").forEach(div => {
  94. let game = {};
  95. game.title = div.querySelector(".heading-text h4").innerText;
  96. const keyfield = div.querySelector(".keyfield");
  97. if (!keyfield) return;
  98. game.key = keyfield.title;
  99. if (game.key.startsWith("Reveal your ")) {
  100. game.key = "";
  101. game.revealed = false;
  102. } else {
  103. game.revealed = true;
  104. }
  105. game.isGift = keyfield.classList.contains("redeemed-gift");
  106. game.isKey = keyfield.classList.contains("redeemed");
  107. games.push(game);
  108. });
  109. return games;
  110. }
  111.  
  112. function refreshOutput(bundle) {
  113. const gameCount = document.querySelectorAll(".keyfield").length;
  114. const revealedCount = document.querySelectorAll(".redeemed,.redeemed-gift").length;
  115.  
  116. const color = gameCount == revealedCount ? "" : "tomato";
  117. let notifyHtml = `Found ${gameCount} keyfields. <span style="background:${color}">${revealedCount} are revealed.</span>`;
  118. if (gameCount != revealedCount) {
  119. notifyHtml += " Are some keys not revealed?";
  120. }
  121.  
  122. if (!bundle.querySelector(".ktt-config-container")) {
  123. const updateCallback = () => refreshOutput(bundle);
  124. const textArea = createArea();
  125. bundle.append(createNotify(), createConfig(updateCallback), textArea, createCopyButton(textArea))
  126. }
  127.  
  128. updateNotify(bundle, notifyHtml)
  129.  
  130. let games = getGames(bundle);
  131. const includeTitle = bundle.querySelector(".includeTitle").checked;
  132. const bundleTitle = includeTitle ? $('h1#hibtext')[0].childNodes[2].textContent.trim() : null;
  133. const includeUnrevealed = bundle.querySelector(".includeUnrevealed").checked;
  134. if (!includeUnrevealed) games = games.filter(e => e.key);
  135. const outputText = formatGames(games, bundleTitle);
  136. updateArea(bundle, outputText);
  137. }
  138.  
  139. function handlePage() {
  140. document.querySelectorAll(".key-container.wrapper").forEach(refreshOutput);
  141. }
  142.  
  143. function waitForLoad(query, callback) {
  144. if (document.querySelector(query)) {
  145. callback();
  146. } else {
  147. setTimeout(waitForLoad.bind(null, query, callback), 100);
  148. }
  149. }
  150.  
  151. waitForLoad(".key-redeemer", handlePage);
  152. })();