Fanatical Keys Backup

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Fanatical Keys Backup
// @namespace    Lex@GreasyFork
// @version      0.3.0
// @description  Displays a text area with game titles and keys so you can copy them out easily.
// @author       Lex
// @match        https://www.fanatical.com/en/orders*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Formats games array to a string to be displayed
    // Games is an array [ [title, key], ... ]
    function formatGames(games, includeUnrevealed, bundleTitle) {
        if (!includeUnrevealed)
            games = games.filter(e => e.gameKey);
        // Format the output as tab-separated
        if (bundleTitle) {
            games = games.map(e => bundleTitle + "\t" + e.gameTitle + "\t" + e.gameKey);
        } else {
            games = games.map(e => e.gameTitle + "\t" + e.gameKey);
        }
        return games.join("\n");
    }

    function revealAllKeys(articles) {
        articles.filter(a => !a.gameKey).forEach(a => {
          a.element.querySelector(".key-container button").click();
        });
    }

    function createRevealButton(bundle) {
        const btn = document.createElement("button");
        btn.type = "button"; // no default behavior
        btn.innerText = "Reveal this bundle's keys";
        btn.addEventListener("click", () => {
            revealAllKeys(bundle.articles);
            btn.style.display = "none";
        })
        return btn;
    }

    function createCopyButton(area) {
        const btn = document.createElement("button");
        btn.type = "button";
        btn.textContent = "Copy to Clipboard";
        btn.style.cssText = "display: block; margin: 5px 0; padding: 5px 10px; cursor: pointer;";
        btn.addEventListener("click", async () => {
            await navigator.clipboard.writeText(area.value);
            btn.textContent = "Copied!";
            setTimeout(() => (btn.textContent = "Copy to Clipboard"), 1500);
        });
        return btn;
    }

    function createConfig(updateCallback) {
        const createCheckbox = (labelText, className, defaultChecked) => {
            const label = document.createElement("label");
            label.style.marginRight = "10px";

            const checkbox = document.createElement("input");
            checkbox.type = "checkbox";
            checkbox.className = className;
            checkbox.checked = defaultChecked;
            checkbox.addEventListener("change", updateCallback);

            label.append(` ${labelText} `, checkbox,);
            return label;
        };

        const container = document.createElement("div");
        container.append(
            createCheckbox("Include Bundle Title", "includeTitle", false),
            createCheckbox("Include Unrevealed", "includeUnrevealed", false)
        );
        container.className = "ktt-config-container"
        return container;
    }

    // Adds a textarea to the bottom of the games listing with all the titles and keys
    function handleBundle(bundle) {
        const games = bundle.articles;
        const keyCount = games.filter(e => e.gameKey).length;

        const lastArticleElement = bundle.articles[bundle.articles.length - 1].element;
        let div = lastArticleElement.nextElementSibling;
        if (!div || div.className !== "ktt-output-container") {
            div = document.createElement("div")
            div.className = "ktt-output-container"
            div.style.width = "100%";
            lastArticleElement.insertAdjacentElement('afterend', div);

            if (games.length != keyCount) {
                div.append(createRevealButton(bundle));
            }

            const notify = document.createElement("div");
            notify.className = "ktt-notify";

            const configCallback = () => { refreshOutput(); };

            const area = document.createElement("textarea");
            area.className = "ktt-area";
            area.style.width = "100%";
            area.setAttribute('readonly', true);
            div.append(notify, createConfig(configCallback), area, createCopyButton(area));
        }

        const color = games.length === keyCount ? "" : "tomato";
        let newInner = `Dumping keys for ${bundle.name}: Found ${games.length} items and <span style="background-color:${color}">${keyCount} keys</span>.`;
        if (games.length != keyCount) {
            newInner += " Are some keys not revealed?";
        }
        const notify = div.querySelector(".ktt-notify");
        if (notify.innerHTML != newInner) {
            notify.innerHTML = newInner;
        }

        const area = div.querySelector(".ktt-area");
        const includeTitle = div.querySelector(".includeTitle").checked;
        const includeUnrevealed = div.querySelector(".includeUnrevealed").checked;
        const gameStr = formatGames(games, includeUnrevealed, includeTitle ? bundle.name : "");
        if (area.value != gameStr) {
            area.value = gameStr;
            // Adjust the height so all the contents are visible
            area.style.height = "";
            area.style.height = area.scrollHeight + 20 + "px";
        }
    }

    function refreshOutput() {
        let currentBundle = null;
        const bundles = [];

        function traverse(element) {
            if (!element) return;
            if (element.matches("section")) {
                const bundleContainer = element.querySelector(".bundle-name-container");
                if (bundleContainer) {
                    const bundleTitle = bundleContainer.textContent.trim();
                    if (currentBundle && currentBundle.articles.length === 0) {
                        currentBundle.name = bundleTitle;
                    } else {
                        currentBundle = {
                            name: bundleTitle,
                            articles: []
                        };
                        bundles.push(currentBundle);
                    }
                }
            }

            if (element.matches("article")) {
                if (!currentBundle) {
                    currentBundle = {
                        name: "unknown",
                        articles: []
                    }
                    bundles.push(currentBundle)
                }
                currentBundle.articles.push({
                    element,
                    gameTitle: element.querySelector(".game-name")?.textContent.trim() ?? "",
                    gameKey: element.querySelector("[aria-label='reveal-key']")?.value ?? "",
                });
                return; // Stop traversing further inside this article
            }

            for (const child of element.children) {
                if (child)
                    traverse(child);
            }
        }
        const container = document.querySelector("section.single-order");
        traverse(container);

        bundles.forEach(handleBundle);

        return bundles;
    }

    let loopCount = 0;
    function handleOrderPage() {
        const bundles = refreshOutput();

        if (bundles.length > 0) {
            if (loopCount++ < 100) {
              setTimeout(handleOrderPage, 500);
            }
        } else {
            if (loopCount++ < 100) {
                setTimeout(handleOrderPage, 100);
            }
        }
    }

    handleOrderPage();
})();