Fanatical Keys Backup

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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();
})();