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.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

Advertisement:

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

Advertisement:

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