AntiFandom: Alternative Wiki Redirector

Finds alternative wikis to use instead of fandom, gives you the choice if several are found and brings you to a cleaner version of the fandom page if none are found.

// ==UserScript==
// @name         AntiFandom: Alternative Wiki Redirector
// @namespace    http://tampermonkey.net/
// @version      1.12
// @description  Finds alternative wikis to use instead of fandom, gives you the choice if several are found and brings you to a cleaner version of the fandom page if none are found.
// @author       Emily
// @match        https://*.fandom.com/wiki*
// @icon         https://www.google.com/s2/favicons?domain=fandom.com
// @grant        GM_xmlhttpRequest
// @connect      *
// @run-at       document-start
// @license      CC0-1.0 license
// ==/UserScript==

(function () {
    "use strict";

    // -------------------------------
    // STEP 0 – Manual Alternative Overrides Config
    // -------------------------------
    // Here we define manual overrides for specific wikis
    // For Lord of the rings the manual alternative will always go to tolkiengateway.net
    const manualOverrides = {
        "lotr": {
            alternatives: [
                {
                    name: "TolkienGateway",
                    base: "https://tolkiengateway.net",
                    pathPrefix: "/wiki/",
                    // For exact pages return the page name; for categories which don't have easily reformattable addresses we redirect to the homepage, it's not perfect but better than nothing.
                    transform: function(page) {
                        return page.startsWith("Category:") ? null : page;
                    },
                    homepage: "https://tolkiengateway.net/wiki/Main_Page"
                }
            ]
        },
        // For hearthstone-archive we force redirection back to hearthstone.wiki.gg as we do for the regular hearthstone fandom wiki
        "hearthstone-archive": {
            alternatives: [
                {
                    name: "Hearthstone.wiki.gg",
                    base: "https://hearthstone.wiki.gg",
                    pathPrefix: "/wiki/",
                    transform: function(page) {
                        return page; // Always use the exact page.
                    },
                    homepage: "https://hearthstone.wiki.gg/"
                }
            ]
        }
    };

    // -------------------------------
    // STEP 1 – Extract GameName and Page Information
    // -------------------------------
    const currentURL = window.location.href;
    if (!currentURL.includes("fandom.com")) return; // Ensure we're on a Fandom page.

    // GameName is extracted from the subdomain (e.g. "hearthstone" from "hearthstone.fandom.com")
    const hostnameParts = window.location.hostname.split(".");
    const gameName = hostnameParts[0];

    // Extract the page portion (everything after "/wiki/")
    const wikiPrefix = "/wiki/";
    let rawPage = "";
    if (window.location.pathname.indexOf(wikiPrefix) === 0) {
        rawPage = window.location.pathname.substring(wikiPrefix.length);
    } else {
        console.error("URL format not recognized. Expected '/wiki/' in the path.");
        return;
    }

    // -------------------------------
    // STEP 2 – Define Default Alternative Wiki Formats
    // -------------------------------
    const alternatives = [
        {
            name: "Wiki.gg",
            base: `https://${gameName}.wiki.gg`,
            pathPrefix: "/wiki/",
            transform: (page) => page  // No transformation needed.
        },
        {
            name: ".wiki",
            base: `https://${gameName}.wiki`,
            pathPrefix: "/w/",
            transform: (page) => page  // No transformation needed.
        },
        {
            name: "Wiki.com",
            base: `https://wiki.${gameName}.com`,
            pathPrefix: "/en-us/",
            transform: (page) => page  // No transformation needed.
        },
        {
            name: "Fextralife.com",
            base: `https://${gameName}.wiki.fextralife.com`,
            pathPrefix: "/",
            transform: (page) => {
                if (page.startsWith("Category:")) {
                    const catContent = page.substring("Category:".length);
                    const index = catContent.indexOf("_");
                    return index !== -1 ? catContent.substring(0, index).trim() : catContent.trim();
                } else {
                    // For normal pages, replace underscores with plus signs. It's a fextralife specific thing for how they handle spaces.
                    return page.replace(/_/g, "+");
                }
            }
        }
    ];

    // -------------------------------
    // STEP 3 – Check URL Availability
    // -------------------------------
    /**
     * Uses GM_xmlhttpRequest to send a HEAD request.
     * Ensures resolution (true if status is 200–399) within 1500ms by using:
     *   - A manual timer
     *   - An explicit ontimeout callback (there's some fandom wikis with no match that otherwise get stuck)
     *
     * @param {string} url - The URL to check.
     * @returns {Promise<boolean>} - Resolves true if the page exists.
     */
    function checkPageExists(url) {
        return new Promise((resolve) => {
            let resolved = false;
            const TIMEOUT_MS = 1500;
            const timer = setTimeout(() => {
                if (!resolved) {
                    resolved = true;
                    console.warn("Manual timeout reached for: " + url);
                    resolve(false);
                }
            }, TIMEOUT_MS);

            GM_xmlhttpRequest({
                method: "HEAD",
                url: url,
                timeout: TIMEOUT_MS,
                ontimeout: () => {
                    if (!resolved) {
                        resolved = true;
                        clearTimeout(timer);
                        console.warn("GM_xmlhttpRequest timeout for: " + url);
                        resolve(false);
                    }
                },
                onload: (response) => {
                    if (!resolved) {
                        resolved = true;
                        clearTimeout(timer);
                        resolve(response.status >= 200 && response.status < 400);
                    }
                },
                onerror: () => {
                    if (!resolved) {
                        resolved = true;
                        clearTimeout(timer);
                        resolve(false);
                    }
                }
            });
        });
    }

    // -------------------------------
    // STEP 4 – Check a Default Alternative Object
    // -------------------------------
    /**
     * For a given alternative (from the default list), check concurrently:
     *   - The full page: alt.base + alt.pathPrefix + alt.transform(rawPage)
     *   - The homepage: alt.base + "/"
     *
     * Returns an object { index, type, url, name }.
     */
    async function checkAlternative(alt, index) {
        const transformedPage = alt.transform(rawPage);
        const fullUrl = alt.base + alt.pathPrefix + transformedPage;
        const domainUrl = alt.base + "/";

        // Execute both check requests in parallel.
        const [pageExists, domainExists] = await Promise.all([
            checkPageExists(fullUrl),
            checkPageExists(domainUrl)
        ]);

        if (pageExists) {
            return { index, type: "page", url: fullUrl, name: alt.name };
        } else if (domainExists) {
            return { index, type: "domain", url: domainUrl, name: alt.name };
        } else {
            return { index, type: "none", url: null, name: alt.name };
        }
    }

    // -------------------------------
    // STEP 5 – Attempt Redirection Combining Default and Manual Overrides.
    // -------------------------------
    async function attemptRedirect() {
        // Check default alternatives concurrently, but use Promise.allSettled so that unfulfilled promises don't block us.
        const autoPromises = alternatives.map((alt, i) => checkAlternative(alt, i));
        const autoResultsSettled = await Promise.allSettled(autoPromises);
        let autoResults = autoResultsSettled
            .filter(r => r.status === "fulfilled")
            .map(r => r.value)
            .filter(r => r.type !== "none");

        // Check manual overrides if present for this wiki.
        let manualResults = [];
        if (manualOverrides.hasOwnProperty(gameName)) {
            const manualAlts = manualOverrides[gameName].alternatives;
            for (const m of manualAlts) {
                const transformed = m.transform(rawPage);
                // If transformed use the homepage.
                const url = transformed ? m.base + m.pathPrefix + transformed : m.homepage;
                if (await checkPageExists(url)) {
                    // Use a high index to let default (auto) alternatives take precedence if available.
                    manualResults.push({ index: 1000, type: "manual", url: url, name: m.name });
                }
            }
        }
        // Combine default and manual results.
        const combined = autoResults.concat(manualResults);

        if (combined.length > 1) {
            // More than one alternative was found: offer a choice overlay.
            displayChoiceOverlay(combined);
        } else if (combined.length === 1) {
            // Exactly one alternative: redirect automatically.
            const chosen = combined[0];
            console.log(`[${chosen.name}] Selected (${chosen.type}) – redirecting to: ${chosen.url}`);
            window.stop();
            window.location.replace(chosen.url);
        } else {
            // If no alternative is found, fall back to AntiFandom.
            console.error("No alternative wiki found; redirecting to AntiFandom.");
            showOverlayMessage("No alternative wiki found, proceeding with AntiFandom...");

            const antifandomUrl = `https://antifandom.com/${gameName}/wiki/${rawPage}`;

            // Redirect to AntiFandom after a short delay.
            setTimeout(() => {
                window.location.replace(antifandomUrl);
            }, 2000);
        }
    }

    // -------------------------------
    // STEP 5.1 – Display Choice Overlay (for Multiple Alternatives)
    // -------------------------------
    function displayChoiceOverlay(choices) {
        // Create an overlay with clickable buttons for each available alternative.
        let overlay = document.getElementById("redirectOverlay");
        if (!overlay) {
            overlay = document.createElement("div");
            overlay.id = "redirectOverlay";
            overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: black; color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: sans-serif; font-size: 24px; z-index: 9999;";
            document.body.appendChild(overlay);
        }
        let content = `<div style="font-size: 64px; margin-bottom: 20px;">🔎</div>`;
        content += `<div>Multiple alternative wikis found.<br>Please choose one:</div>`;
        content += `<div style="margin-top: 20px;">`;
        choices.forEach(choice => {
            // When clicked, the button immediately redirects.
            content += `<button style="margin: 10px; padding: 10px 15px; font-size: 18px; cursor: pointer;" data-url="${choice.url}">${choice.name}</button>`;
        });
        content += `</div>`;
        overlay.innerHTML = content;

        // Add event listeners to buttons after they are added to the DOM.
        overlay.querySelectorAll("button[data-url]").forEach(button => {
            button.addEventListener("click", () => {
                const url = button.getAttribute("data-url");
                window.location.replace(url);
            });
        });
    }
    window.displayChoiceOverlay = displayChoiceOverlay;

    // -------------------------------
    // STEP 6 – Overlay: Create a Message Overlay
    // -------------------------------
    /**
     * Creates (or updates) a floating overlay that covers the page with the given text
     * and displays a large magnifying glass emoji (🔎).
     *
     * @param {string} message - The message to display.
     */
    function showOverlayMessage(message) {
        const createOverlay = () => {
            let overlay = document.getElementById("redirectOverlay");
            const unicodeMagnifyingGlass = `<div style="font-size: 64px; margin-bottom: 20px;">🔎</div>`;
            const content = unicodeMagnifyingGlass + `<div>${message}</div>`;

            if (!overlay) {
                overlay = document.createElement("div");
                overlay.id = "redirectOverlay";
                overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: black; color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: sans-serif; font-size: 24px; z-index: 9999;";
                overlay.innerHTML = content;
                document.body.appendChild(overlay);
            } else {
                overlay.innerHTML = content;
            }
        };

        if (document.body) {
            createOverlay();
        } else {
            // Wait for the DOM to be ready if document.body is not available.
            const observer = new MutationObserver(() => {
                if (document.body) {
                    observer.disconnect();
                    createOverlay();
                }
            });
            observer.observe(document.documentElement, { childList: true });
        }
    }

    // -------------------------------
    // STEP 7 – Start the Process.
    // -------------------------------
    attemptRedirect().catch((err) => {
        console.error("Error in attemptRedirect:", err);
        showOverlayMessage("An error occurred. Please try again later.");
    });

    if (document.body) {
        // If the body is already available, show the overlay immediately.
        showOverlayMessage("Searching for an alternative wiki…");
    } else {
        // If the body is not yet available, observe for its creation.
        const observer = new MutationObserver(() => {
            if (document.body) {
                observer.disconnect(); // Stop observing once the body is available.
                showOverlayMessage("Searching for an alternative wiki…");
            }
        });
        observer.observe(document.documentElement, { childList: true });
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址