MSPFA extras

Adds custom quality of life features to MSPFA.

目前為 2020-11-28 提交的版本,檢視 最新版本

// ==UserScript==
// @name         MSPFA extras
// @namespace    http://tampermonkey.net/
// @version      1.7.5.1
// @description  Adds custom quality of life features to MSPFA.
// @author       seymour schlong
// @icon         https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/ico.png
// @icon64       https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/ico.png
// @match        https://mspfa.com/
// @match        https://mspfa.com/*/
// @match        https://mspfa.com/*/?*
// @match        https://mspfa.com/?s=*
// @match        https://mspfa.com/my/*
// @match        https://mspfa.com/random/
// @exclude      https://mspfa.com/js/*
// @exclude      https://mspfa.com/css/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const currentVersion = "1.7.5.1";
    console.log(`MSPFA extras script v${currentVersion} by seymour schlong`);

    const debug = false;

    /**
    * https://github.com/GrantGryczan/MSPFA/projects/1?fullscreen=true
    * Github to-do completion list (and other stuff too)
    *
    * https://github.com/GrantGryczan/MSPFA/issues/26 - Dropdown menu                   - February 23rd, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/18 - MSPFA themes                    - February 23rd, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/32 - Adventure creation dates        - February 23rd, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/32 - User creation dates             - February 23rd, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/40 - Turn certain buttons into links - July 21st, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/41 - Word and character count        - July 21st, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/57 - Default spoiler values          - August 7th, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/62 - Buttonless spoilers             - August 7th, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/52 - Hash URLs                       - August 8th, 2020
    *                                                 - Page drafts                     - August 8th, 2020
    *                                                 - Edit pages button               - August 8th, 2020
    *                                                 - Image preloading                - August 20th, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/19 - Manage game saves               - August 22nd, 2020
    *
    * Extension to-do... maybe...
    *
    * If trying to save a page and any other save button is not disabled, ask the user if they would rather Save All instead, or prompt to disable update notifications.
    *     When adding a new page, store it in an array and if that array length is > 1 when someone tries to save, prompt them to press Save All?
    */

    // A general function that allows for waiting until a certain element appears on the page.
    const pageLoad = (fn, length) => {
        const interval = setInterval(() => {
            if (fn()) clearInterval(interval);
        }, length ? length*1000 : 500);
    };

    // Saves the options data for the script.
    const saveData = (data) => {
        localStorage.mspfaextra = JSON.stringify(data);
        if (debug) {
            console.log('Settings:');
            console.log(data);
        }
    };

    // Saves the data for drafts
    const saveDrafts = (data) => {
        localStorage.mspfadrafts = JSON.stringify(data);
        if (debug) {
            console.log('Drafts:');
            console.log(data);
        }
    };

    // Encases an element within a link
    const addLink = (elm, url, target) => {
        const link = document.createElement('a');
        link.href = url;
        link.draggable = false;
        if (elm.parentNode) elm.parentNode.insertBefore(link, elm);
        if (target) link.target = target;
        link.appendChild(elm);
        return link;
    };

    // Easy br element
    const newBr = () => {
        return document.createElement('br');
    }

    // Make creating label elements easier
    const createLabel = (text, id) => {
        const newLabel = document.createElement('label');
        newLabel.textContent = text;
        newLabel.setAttribute('for', id);
        return newLabel;
    }

    let settings = {};
    let drafts = {};

    const defaultSettings = {
        autospoiler: false,
        style: 0,
        styleURL: "",
        night: false,
        auto502: true,
        textFix: false,
        pixelFix: false,
        intro: false,
        commandScroll: false,
        preload: true,
        dialogKeys: true,
        dialogFocus: false,
        spoilerValues: {}
    }

    let pageLoaded = false;

    const loadDrafts = () => {
        if (localStorage.mspfadrafts) {
            drafts = JSON.parse(localStorage.mspfadrafts);
        }
    }
    loadDrafts();

    // Load any previous settings from localStorage
    if (localStorage.mspfaextra) {
        Object.assign(settings, JSON.parse(localStorage.mspfaextra));

        // Get draft data from settings
        if (typeof settings.drafts === "object") {
            if (Object.keys(settings.drafts).length > 0 && Object.keys(drafts).length === 0) {
                drafts = settings.drafts;
            }
        }
        saveDrafts(drafts);
    }

    // If any settings are undefined, re-set to their default state. (For older users when new things get stored)
    const checkSettings = () => {
        const defaultSettingsKeys = Object.keys(defaultSettings);
        for (let i = 0; i < defaultSettingsKeys.length; i++) {
            if (typeof settings[defaultSettingsKeys[i]] === "undefined") {
                settings[defaultSettingsKeys[i]] = defaultSettings[defaultSettingsKeys[i]];
            }
        }
        saveData(settings);
    }

    checkSettings();

    if (GM_info && GM_info.scriptHandler !== "Tampermonkey" && !settings.warned) {
        alert(`It appears that you're running the MSPFA extras script with ${GM_info.scriptHandler}.\nUnfortunately, this script cannot run at its full potential because of that.\nTry switching to Tampermonkey if you want to use more of the features!\n(this message will only appear once.)`);
        settings.warned = true;
        saveData(settings);
    }

    // Scrolls you to where you need to be
    const hashSearch = location.href.replace(location.origin + location.pathname, '').replace(location.search, '');
    if (hashSearch !== '') {
        pageLoad(() => {
            const idElement = document.querySelector(hashSearch);
            if (idElement) {
                const selected = document.querySelector(hashSearch);
                selected.scrollIntoView();
                selected.style.outline = '3px solid black';
                selected.style.transition = '0.5s';
                pageLoad(() => {
                    if (pageLoaded) {
                        selected.style.outline = '0px solid black';
                    }
                });

                return true;
            }
        }, 1);
    }

    // Ripped shamelessly right from mspfa lol (URL search parameters -- story ID, page num, etc.)
    let rawParams;
    if (location.href.indexOf("#") != -1) {
        rawParams = location.href.slice(0, location.href.indexOf("#"));
    } else {
        rawParams = location.href;
    }
    if (rawParams.indexOf("?") != -1) {
        rawParams = rawParams.slice(rawParams.indexOf("?") + 1).split("&");
    } else {
        rawParams = [];
    }
    const params = {};
    for (let i = 0; i < rawParams.length; i++) {
        try {
            const p = rawParams[i].split("=");
            params[p[0]] = decodeURIComponent(p[1]);
        } catch (err) {}
    }

    if (debug) {
        console.log('URL parameters:');
        console.log(params);
    }

    // Functions to get/change data from the console
    window.MSPFAe = {
        getSettings: () => {
            return settings;
        },
        getSettingsString: (formatted) => {
            if (formatted) {
                console.log(JSON.stringify(settings, null, 4));
            } else {
                console.log(JSON.stringify(settings));
            }
        },
        getDrafts: () => {
            loadDrafts();
            return drafts;
        },
        getDraftsString: (formatted) => {
            loadDrafts();
            if (formatted) {
                console.log(JSON.stringify(drafts, null, 4));
            } else {
                console.log(JSON.stringify(drafts));
            }
        },
        changeSettings: (newSettings) => {
            console.log('Settings updated');
            console.log(settings);
            Object.assign(settings, newSettings);
            saveData(settings);
        },
        changeSettingsString: (fullString) => {
            try {
                JSON.parse(fullString);
            } catch (err) {
                console.error(err);
                return;
            }
            settings = JSON.parse(fullString);
            checkSettings();
            console.log(settings);
        },
        getParams: params
    }

    // Error reloading
    window.addEventListener("load", () => {
        // Reload the page if 502 CloudFlare error page appears
        if (settings.auto502 && document.querySelector('#cf-wrapper')) {
            window.location.reload();
        }

        // Wait five seconds, then refresh the page
        if (document.body.textContent === "Your client is sending data to MSPFA too quickly. Wait a moment before continuing.") {
            setTimeout(() => {
                window.location.reload();
            }, 5000);
        }

        pageLoaded = true;
    });

    // Delete any unchanged spoiler values
    if (location.pathname !== "/my/stories/pages/") {
        // Go through spoiler values and remove any that aren't unique
        Object.keys(settings.spoilerValues).forEach(adventure => {
            if (settings.spoilerValues[adventure].open === "Show" && settings.spoilerValues[adventure].close === "Hide") {
                delete settings.spoilerValues[adventure];
            } else if (settings.spoilerValues[adventure].open === '' && settings.spoilerValues[adventure].close === '') {
                delete settings.spoilerValues[adventure];
            }
        });
    }

    const styleOptions = ["Standard", "Low Contrast", "Light", "Dark", "Felt", "Trickster", "Custom"];
    const styleUrls = ['', '/css/theme1.css', '/css/theme2.css', 'https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/themes/dark.css', '/css/theme4.css', '/css/theme5.css'];

    const createDropdown = (parent, explore) => {
        const dropDiv = document.createElement('div');
        dropDiv.className = 'dropdown';
        dropDiv.style.display = 'inline-block';

        const dropContent = document.createElement('div');
        dropContent.className = 'dropdown-content';
        dropContent.style.display = 'none';

        dropDiv.addEventListener('mouseenter', evt => {
            dropContent.style.display = 'block';
            dropContent.style.color = getComputedStyle(parent).color;
            dropContent.querySelectorAll('a').forEach(link => {
                link.style.color = getComputedStyle(parent).color;
            });
        });

        if (!explore) {
            dropDiv.addEventListener('mouseleave', evt => {
                dropContent.style.display = 'none';
            });
        }

        parent.parentNode.insertBefore(dropDiv, parent);
        dropDiv.appendChild(parent);
        dropDiv.appendChild(dropContent);
        return [dropDiv, dropContent];
    }

    // "MY MSPFA" dropdown
    const myLink = document.querySelector('nav a[href="/my/"]');
    if (myLink) {
        const dropContent = createDropdown(myLink)[1];

        const dLinks = [];
        dLinks[0] = [ 'Messages', 'My Adventures', 'Settings' ];
        dLinks[1] = [ '/my/messages/', '/my/stories/', '/my/settings/' ];

        for (let i = 0; i < dLinks[0].length; i++) {
            const newLink = document.createElement('a');
            newLink.textContent = dLinks[0][i];
            newLink.href = dLinks[1][i];
            dropContent.appendChild(newLink);
        }

        // Append "My Profile" to the dropdown list if you're signed in
        pageLoad(() => {
            if (window.MSPFA) {
                if (window.MSPFA.me.n) {
                    const newFavLink = document.createElement('a');
                    newFavLink.textContent = "My Favourites";
                    newFavLink.href = `/favs/?u=${window.MSPFA.me.i}`;

                    const newMyLink = document.createElement('a');
                    newMyLink.textContent = "My Profile";
                    newMyLink.href = `/user/?u=${window.MSPFA.me.i}`;

                    dropContent.appendChild(newFavLink);
                    dropContent.appendChild(newMyLink);

                    // Move SETTINGS to the bottom
                    dropContent.appendChild(dropContent.querySelectorAll('a')[2]);
                    return true;
                }
            }
        },.1);
    }

    // "RANDOM" dropdown
    const randomLink = document.querySelector('nav a[href="/random/"]');
    if (randomLink) {
        // Thank you @MadCreativity 🙏
        const dropContent = createDropdown(randomLink)[1];

        (async () => {
            const dLinks = [];
            dLinks[0] = [ 'Recent ongoing' ];
            dLinks[1] = [ await fetch(`https://mspfa-extras-server.herokuapp.com/api/random`).then(e => e.text()) ];

            for (let i = 0; i < dLinks[0].length; i++) {
                const newLink = document.createElement('a');
                newLink.textContent = dLinks[0][i];
                newLink.href = dLinks[1][i];
                dropContent.appendChild(newLink);
            }
        })()
    }

    // "EXPLORE" dropdown
    const exploreLink = document.querySelector('nav a[href="/stories/"');
    if (exploreLink) {
        const dropdown = createDropdown(exploreLink, true);
        const dropDiv = dropdown[0];
        const dropContent = dropdown[1];

        const userLink = document.createElement('a');
        userLink.textContent = 'User Search';
        userLink.href = '/?s=36596&p=7'
        dropContent.appendChild(userLink);

        const exploreInput = document.createElement('input');
        Object.assign(exploreInput, { type: 'text', placeholder: 'Search...', id: 'dropdown-explore' });
        dropContent.appendChild(exploreInput);
        exploreInput.addEventListener('keydown', ke => {
            if (ke.code === 'Enter') {
                const searchLink = `/stories/?go=1&n=${encodeURIComponent(exploreInput.value)}&t=&h=14&o=favs&p=p&m=50&load=true`;
                if (ke.altKey || ke.ctrlKey) {
                    window.open(searchLink, '_blank').focus();
                } else {
                    location.href = searchLink;
                }
                return;
            }
        });
        dropDiv.addEventListener('mouseleave', evt => {
            // If input is focused
            if (document.activeElement !== exploreInput) {
                dropContent.style.display = 'none';
            }
        });
        document.body.addEventListener('click', evt => {
            if (document.activeElement !== exploreInput) {
                dropContent.style.display = 'none';
            }
        });
    }

    document.querySelector('header .mspfalogo').parentNode.draggable = false;
    addLink(document.querySelector('footer .mspfalogo'), 'javascript:void(0);');

    // Message that shows when you first get the script
    const showIntroDialog = () => {
        const msg = window.MSPFA.parseBBCode('Hi! Thanks for installing this script!\n\nBe sure to check the [url=https://greasyfork.org/en/scripts/396798-mspfa-extras#additional-info]GreasyFork[/url] page to see a full list of features, and don\'t forget to check out your [url=https://mspfa.com/my/settings/#extraSettings]settings[/url] page to tweak things to how you want.\n\nIf you have any suggestions, or you find a bug, please be sure to let me know on Discord at [url=discord://discordapp.com/users/277928549866799125]@seymour schlong#3669[/url].\n\n[size=12]This dialog will only appear once. To view it again, click "View Script Message" at the bottom of the site.[/size]');
        window.MSPFA.dialog("MSPFA extras message", msg, ["Okay"]);
    }

    // Check if show intro dialog has displayed
    if (!settings.intro) {
        pageLoad(() => {
            if (window.MSPFA) {
                showIntroDialog();
                settings.intro = true;
                saveData(settings);
                return true;
            }
        });
    }

    const details = document.querySelector('#details');

    // Add 'link' at the bottom to show the intro dialog again
    const introLink = document.createElement('a');
    introLink.textContent = 'View Script Message';
    introLink.href = 'javascript:void(0);';
    introLink.addEventListener('click', showIntroDialog);
    details.appendChild(introLink);

    // vbar!!!!
    const vbar = document.createElement('span');
    Object.assign(vbar, {className: 'vbar', textContent: '|'});
    details.appendChild(document.createTextNode(' '));
    details.appendChild(vbar);
    details.appendChild(document.createTextNode(' '));

    // if you really enjoy the script and has some extra moneys 🥺
    const donateLink = document.createElement('a');
    donateLink.textContent = 'Donate';
    donateLink.href = 'https://ko-fi.com/ironbean';
    donateLink.target = "blank";
    details.appendChild(donateLink);

    // Theme stuff
    const theme = document.createElement('link');
    Object.assign(theme, { id: 'theme', type: 'text/css', rel: 'stylesheet' });
    const updateTheme = (src) => {
        theme.href = src;
    }
    if (!document.querySelector('#theme')) {
        document.querySelector('head').appendChild(theme);
        if (settings.night) {
            updateTheme(styleUrls[3]);
        } else {
            updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
        }
    }

    const pixelText = () => {
        return settings.pixelFix ? 'body { image-rendering: pixelated; image-rendering: -moz-crisp-edges; } .cellicon { image-rendering: -webkit-optimize-contrast !important; }' : '';
    }

    // Dropdown menu and pixelated scaling
    const mspfaeCSS = document.createElement('link');
    Object.assign(mspfaeCSS, { id: 'script-css', type: 'text/css', rel: 'stylesheet', href: 'https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/mspfae.css' });
    document.querySelector('head').appendChild(mspfaeCSS);

    const extraStyle = document.createElement('style');
    if (!document.querySelector('#extra-style')) {
        extraStyle.id = 'extra-style';
        extraStyle.textContent = pixelText();
        document.querySelector('head').appendChild(extraStyle);
    }

    let nightSwitch = [];

    // Enabling night mode.
    document.querySelector('footer .mspfalogo').addEventListener('click', evt => {
        settings.night = !settings.night;
        saveData(settings);

        for (let i = 0; i < nightSwitch.length; i++) {
            clearTimeout(nightSwitch[i]);
        }
        nightSwitch = [];

        // Transition to make it feel nicer on the eyes
        extraStyle.textContent = pixelText();
        extraStyle.textContent = pixelText() + ' *{transition:1.5s;}';

        if (settings.night) {
            updateTheme(styleUrls[3]);
        } else {
            updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
        }

        nightSwitch.push(setTimeout(() => {
            extraStyle.textContent = pixelText();
        }, 1500));
    });

    // Enable keyboard controls for some dialog boxes (enter/esc to accept/close)
    const dialog = document.querySelector('#dialog');
    document.addEventListener('keydown', evt => {
        if (settings.dialogKeys && !dialog.textContent.includes('BBCode')) {
            if (dialog.style.display === '' && (evt.code === 'Enter' || evt.code === "Escape") && (document.activeElement === document.body || settings.dialogFocus)) {
                let buttons = dialog.querySelectorAll('button');
                if (buttons.length === 1) {
                    buttons[0].click();
                } else if (buttons.length === 2) {
                    if (["Okay", "Yes"].indexOf(buttons[0].textContent) !== -1 && evt.code === "Enter") {
                        buttons[0].click();
                    }
                }
                if (["Cancel", "Close", "No"].indexOf(buttons[buttons.length - 1].textContent) !== -1 && evt.code === "Escape") {
                    buttons[buttons.length - 1].click();
                }
            }
        }
    });

    if (location.pathname.includes('//')) {
        location.href = location.pathname.replace(/\/\//g, '/') + location.search;
    }

    if (location.pathname === "/" || location.pathname === "/preview/") {
        if (location.search) {
            // Remove the current theme if the adventure has CSS (to prevent conflicts);
            if (settings.style > 0) {
                pageLoad(() => {
                    if (window.MSPFA) {
                        if (window.MSPFA.story && window.MSPFA.story.y && (window.MSPFA.story.y.toLowerCase().includes('import') || window.MSPFA.story.y.includes('{'))) {
                            if (!settings.night) updateTheme('');
                            return true;
                        }
                    }
                    if (pageLoaded) return true;
                });
            }

            // Preload adjacent pages
            if (settings.preload) {
                const preloadImages = document.createElement('div');
                preloadImages.id = 'preload';
                document.querySelector('#container').appendChild(preloadImages);
                window.MSPFA.slide.push(p => {
                    preloadImages.innerHTML = '';
                    if (window.MSPFA.story.p[p-2]) {
                        let page = window.MSPFA.parseBBCode(window.MSPFA.story.p[p-2].b);
                        page.querySelectorAll('img').forEach(image => {
                            preloadImages.appendChild(image);
                        });
                        page.innerHTML = '';
                    }
                    if (window.MSPFA.story.p[p]) {
                        let page = window.MSPFA.parseBBCode(window.MSPFA.story.p[p].b);
                        page.querySelectorAll('img').forEach(image => {
                            preloadImages.appendChild(image);
                        });
                        page.innerHTML = '';
                    }
                });
            }

            // Automatic spoiler opening
            if (settings.autospoiler) {
                window.MSPFA.slide.push((p) => {
                    document.querySelectorAll('#slide .spoiler:not(.open) > div:first-child > input').forEach(sb => sb.click());
                });
            }

            // Scroll up to the nav bar when changing page so you don't have to scroll down as much =)
            if (settings.commandScroll) {
                const heightTop = document.querySelector('nav').getBoundingClientRect().top - document.body.getBoundingClientRect().top;
                let temp = -2; // To prevent moving the page down when loading it for the first time
                window.MSPFA.slide.push((p) => {
                    if (temp < 0) {
                        temp++;
                    } else {
                        window.scroll(0, heightTop);
                    }
                });
            }

            // Show creation date
            pageLoad(() => {
                let infoTd = document.querySelector('#infobox tr td:nth-child(2)');
                if (infoTd) {
                    infoTd.appendChild(document.createTextNode('Creation date: ' + new Date(window.MSPFA.story.d).toString().split(' ').splice(1, 3).join(' ')));
                    infoTd.appendChild(newBr());
                    infoTd.appendChild(document.createTextNode('Last update: ' + new Date(window.MSPFA.story.p[window.MSPFA.story.p.length-1].d).toString().split(' ').splice(1, 3).join(' ')));
                    return true;
                }
            });

            // Hash scrolling and opening infobox or commmentbox
            if (['#infobox', '#commentbox', '#newcomment', '#latestpages'].indexOf(hashSearch) !== -1) {
                pageLoad(() => {
                    if (document.querySelector(hashSearch)) {
                        if (hashSearch === '#infobox') {
                            document.querySelector('input[data-open="Show Adventure Info"]').click();
                        } else if (hashSearch === '#commentbox' || hashSearch === '#newcomment') {
                            document.querySelector('input[data-open="Show Comments"]').click();
                        } else if (hashSearch === '#latestpages') {
                            document.querySelector('input[data-open="Show Adventure Info"]').click();
                            document.querySelector('input[data-open="Show Latest Pages"]').click();
                        }
                        return true;
                    }
                });
            }

            // Attempt to fix text errors
            if (settings.textFix && location.pathname !== "/preview/") {
                pageLoad(() => {
                    if (window.MSPFA.story && window.MSPFA.story.p) {
                        // russian/bulgarian is not possible =(
                        const currentPage = parseInt(/^\?s(?:.*?)&p=([\d]*)$/.exec(location.search)[1]);
                        const library = [
                            ["&acirc;��", "'"],
                            ["&Atilde;�", "Ñ"],
                            ["&Atilde;&plusmn;", "ñ"],
                            ["&Atilde;&sup3;", "ó"],
                            ["&Atilde;&iexcl;", "á"],
                            ["&Auml;�", "ą"],
                            ["&Atilde;&shy;", "í"],
                            ["&Atilde;&ordm;", "ú"],
                            ["&Atilde;&copy;", "é"],
                            ["&Aring;�", "ł"],
                            ["&Aring;&frac14;", "ż"],
                            ["&Acirc;&iexcl;", "¡"],
                            ["&Acirc;&iquest;", "¿"],
                            ["N&Acirc;&ordm;", "#"]
                        ];
                        // https://mspfa.com/?s=5280&p=51 -- unknown error

                        const replaceTerms = (p) => {
                            library.forEach(term => {
                                if (window.MSPFA.story.p[p]) {
                                    window.MSPFA.story.p[p].c = window.MSPFA.story.p[p].c.replace(new RegExp(term[0], 'g'), term[1]);
                                    window.MSPFA.story.p[p].b = window.MSPFA.story.p[p].b.replace(new RegExp(term[0], 'g'), term[1]);
                                }
                            });
                        };

                        replaceTerms(currentPage-1);

                        window.MSPFA.slide.push(p => {
                            replaceTerms(p);
                            replaceTerms(p-2);
                        });
                        return true;
                    }
                });
            }

            // Turn buttons into links
            const pageButton = document.createElement('button');
            const pageLink = addLink(pageButton, `/my/stories/pages/?s=${params.s}#p${params.p}`);
            pageButton.className = 'pages edit major';
            pageButton.type = 'button';
            pageButton.title = 'Edit Pages';

            // Edit pages button & button link
            pageLoad(() => {
                const infoButton = document.querySelector('.edit.major');
                if (infoButton) {
                    pageLoad(() => {
                        if (window.MSPFA.me.i) {
                            infoButton.title = "Edit Info";
                            infoButton.parentNode.insertBefore(pageLink, infoButton);
                            infoButton.parentNode.insertBefore(document.createTextNode(' '), infoButton);
                            addLink(infoButton, `/my/stories/info/?s=${params.s}`);
                            pageButton.style.display = document.querySelector('.edit.major:not(.pages)').style.display;

                            // Change change page link when switching pages
                            window.MSPFA.slide.push(p => {
                                const newSearch = location.search.split('&p=');
                                pageLink.href = `/my/stories/pages/?s=${params.s}#p${newSearch[1].split('#')[0]}`;
                            });
                            return true;
                        }
                    });
                    addLink(document.querySelector('.rss.major'), `/rss/?s=${params.s}`);
                    return true;
                }
            });

            // Add "Reply" button next to comment gear
            setInterval(() => {
                if (document.querySelector('#commentbox > .spoiler.open')) {
                    document.querySelectorAll('.gear').forEach(gear => {
                        if (!gear.parentNode.querySelector('.reply')) {
                            const replyDiv = document.createElement('div');
                            replyDiv.className = 'reply';
                            gear.insertAdjacentElement('afterEnd', replyDiv);
                            gear.insertAdjacentHTML('afterEnd', '<span style="float: right"> </span>');
                            const userID = gear.parentNode.parentNode.classList[2].replace('u', '');

                            replyDiv.addEventListener('click', () => {
                                const commentBox = document.querySelector('#commentbox textarea');
                                commentBox.value = `[user]${userID}[/user], ${commentBox.value}`;
                                commentBox.focus();
                                commentBox.parentNode.scrollIntoView();
                            });
                        }
                    });
                }
            }, 500);
        }
    }
    else if (location.pathname === "/my/") {
        const editStories = document.querySelector('#editstories');
        editStories.classList.remove('alt');
        const parent = editStories.parentNode;
        const viewSaves = document.createElement('a');
        Object.assign(viewSaves, { id: 'viewsaves', className: 'major', textContent: 'View Adventure Saves' });

        parent.appendChild(viewSaves);
        parent.appendChild(newBr());
        parent.appendChild(newBr());

        pageLoad(() => {
            if (window.MSPFA && window.MSPFA.me && window.MSPFA.me.i) {
                viewSaves.href = `/?s=36596&p=6`;
                return true;
            }
        },.1);

        const messages = document.querySelector('#messages');

        pageLoad(() => {
            if (messages.textContent.includes('(')) {
                document.title = document.title + messages.textContent.toLowerCase().replace('messages', '');
                return true;
            }
        });
    }
    else if (location.pathname === "/my/settings/") { // Custom settings
        const saveBtn = document.querySelector('#savesettings');

        const table = document.querySelector("#editsettings tbody");
        let saveTr = table.querySelectorAll("tr");
        saveTr = saveTr[saveTr.length - 1];

        const headerTr = document.createElement('tr');
        const header = document.createElement('th');
        Object.assign(header, { id: 'extraSettings', textContent: 'Extra Settings' });
        headerTr.appendChild(header);

        const moreTr = document.createElement('tr');
        const more = document.createElement('td');
        more.textContent = "* This only applies to a select few older adventures that have had their text corrupted. Some punctuation is fixed, as well as regular characters with accents. Currently only some spanish/french is fixable. Russian/Bulgarian is not possible.";
        moreTr.appendChild(more);

        const settingsTr = document.createElement('tr');
        const localMsg = document.createElement('span');
        const settingsTd = document.createElement('td');
        localMsg.innerHTML = "Because this is an extension, any data saved is only <b>locally</b> on this device.<br>Don't forget to <b>save</b> when you've finished making changes!";
        const plusTable = document.createElement('table');
        const plusTbody = document.createElement('tbody');
        plusTable.appendChild(plusTbody);
        settingsTd.appendChild(localMsg);
        settingsTd.appendChild(newBr());
        settingsTd.appendChild(newBr());
        settingsTd.appendChild(plusTable);
        settingsTr.appendChild(settingsTd);

        plusTable.style = "text-align: center;";

        // Create checkbox (soooo much better)
        const createCheckbox = (text, checked, id) => {
            const optionTr = plusTbody.insertRow(plusTbody.childNodes.length);
            const optionTextTd = optionTr.insertCell(0);
            const optionLabel = createLabel(text, id);
            const optionInputTd = optionTr.insertCell(1);
            const optionInput = document.createElement('input');
            optionInputTd.appendChild(optionInput);

            optionTextTd.appendChild(optionLabel);
            optionInput.type = "checkbox";
            optionInput.checked = checked;
            optionInput.id = id;

            return optionInput;
        }

        const spoilerInput = createCheckbox("Automatically open spoilers:", settings.autospoiler, 'autospoiler');
        const preloadInput = createCheckbox("Preload images for the pages immediately before and after:", settings.preload, 'preload');
        const errorInput = createCheckbox("Automatically reload Cloudflare 502 error pages:", settings.auto502, 'auto502');
        const commandScrollInput = createCheckbox("Scroll back up to the nav bar when switching page:", settings.commandScroll, 'commandScroll');
        const dialogKeysInput = createCheckbox("Use enter/escape keys to accept/exit control some dialogs:", settings.dialogKeys, 'dialogKeys');
        const dialogFocusInput = createCheckbox("Let keys work while dialog isn't focused (above required):", settings.dialogFocus, 'dialogFocus');
        const pixelFixInput = createCheckbox("Change pixel scaling to nearest neighbour:", settings.pixelFix, 'pixelFix');
        const textFixInput = createCheckbox("Attempt to fix text errors (experimental)*:", settings.textFix, 'textFix');

        const cssTr = plusTbody.insertRow(plusTbody.childNodes.length);
        const cssTextTd = cssTr.insertCell(0);
        const cssSelectTd = cssTr.insertCell(1);
        const cssSelect = document.createElement('select');
        cssSelectTd.appendChild(cssSelect);

        cssTextTd.textContent = "Change style:";

        const customTr = plusTbody.insertRow(plusTbody.childNodes.length);
        const customTextTd = customTr.insertCell(0);
        const customCssTd = customTr.insertCell(1);
        const customCssInput = document.createElement('input');
        customCssTd.appendChild(customCssInput);

        customTextTd.textContent = "Custom CSS URL:";
        customCssInput.style.width = "99px";
        customCssInput.value = settings.styleURL;

        styleOptions.forEach(o => cssSelect.appendChild(new Option(o, o)));

        saveTr.parentNode.insertBefore(headerTr, saveTr);
        saveTr.parentNode.insertBefore(settingsTr, saveTr);
        saveTr.parentNode.insertBefore(moreTr, saveTr);
        cssSelect.selectedIndex = settings.style;

        const buttonSpan = document.createElement('span');
        const draftButton = document.createElement('input');
        const spoilerButton = document.createElement('input');
        draftButton.value = 'Manage Drafts';
        draftButton.className = 'major';
        draftButton.type = 'button';
        spoilerButton.value = 'Manage Spoiler Values';
        spoilerButton.className = 'major';
        spoilerButton.type = 'button';
        buttonSpan.appendChild(draftButton);
        buttonSpan.appendChild(document.createTextNode(' '));
        buttonSpan.appendChild(spoilerButton);
        settingsTd.appendChild(buttonSpan);

        const draftMsg = window.MSPFA.parseBBCode('Here you can manage the drafts that you have saved for your adventure(s).\n');
        const listTable = document.createElement('table');
        listTable.id = 'draft-table';
        const listTbody = document.createElement('tbody');
        listTable.appendChild(listTbody);

        const draftsEmpty = () => {
            loadDrafts();
            let empty = true;
            Object.keys(drafts).forEach(adv => {
                if (empty) {
                    const length = typeof drafts[adv].cachedTitle === "undefined" ? 0 : 1;
                    if (Object.keys(drafts[adv]).length > length) {
                        empty = false;
                    }
                }
            });
            return empty;
        }

        setInterval(() => {
            draftButton.disabled = draftsEmpty();
        }, 1000);

        draftButton.addEventListener('click', () => {
            draftMsg.appendChild(listTable);
            listTbody.innerHTML = '';
            loadDrafts();

            const addAdv = (story, name) => {
                const storyTr = listTbody.insertRow(listTable.rows);
                const titleLink = document.createElement('a');
                Object.assign(titleLink, { className: 'major', href: `/my/stories/pages/?s=${story}&click=d`, textContent: name, target: '_blank' });
                storyTr.insertCell(0).appendChild(titleLink);
                const deleteButton = document.createElement('input');
                Object.assign(deleteButton, { className: 'major', type: 'button', value: 'Delete' });
                storyTr.insertCell(1).appendChild(deleteButton);

                deleteButton.addEventListener('click', () => {
                    setTimeout(() => {
                        window.MSPFA.dialog('Delete adventure draft?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
                            if (output === "Yes") {
                                loadDrafts();
                                drafts[story] = {};

                                if (settings.drafts && settings.drafts[story]) {
                                    delete settings.drafts[story];
                                    saveData(settings);
                                }

                                saveDrafts(drafts);

                                setTimeout(() => {
                                    draftButton.click();
                                }, 1);

                                if (draftsEmpty) {
                                    draftButton.disabled = true;
                                }
                            }
                        });
                    }, 1);
                });
            }

            Object.keys(drafts).forEach(adv => {
                const length = typeof drafts[adv].cachedTitle === "undefined" ? 0 : 1;
                if (Object.keys(drafts[adv]).length > length) {
                    if (!!length) {
                        addAdv(adv, drafts[adv].cachedTitle);
                    }
                    else {
                        window.MSPFA.request(0, {
                            do: "story",
                            s: adv
                        }, story => {
                            if (typeof story !== "undefined") {
                                console.log(story);
                                addAdv(adv, story.n);
                            }
                        });
                    }
                }
            });

            window.MSPFA.dialog('Manage Drafts', draftMsg, ["Delete All", "Close"], (output, form) => {
                if (output === "Delete All") {
                    setTimeout(() => {
                        window.MSPFA.dialog('Delete all Drafts?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
                            if (output === "Yes") {
                                Object.keys(drafts).forEach(adv => {
                                    drafts[adv] = {};
                                });
                                saveDrafts(drafts);

                                if (typeof settings.drafts !== "undefined") {
                                    delete settings.drafts;
                                    saveData(settings);
                                }

                                draftButton.disabled = true;
                            }
                        });
                    }, 1);
                }
            });
        });

        if (Object.keys(settings.spoilerValues).length === 0) {
            spoilerButton.disabled = true;
        }

        const spoilerMsg = window.MSPFA.parseBBCode('Here you can manage the spoiler values that you have set for your adventure(s).\nClick on an adventure\'s title to see the values.\n');

        spoilerButton.addEventListener('click', () => {
            spoilerMsg.appendChild(listTable);
            listTbody.innerHTML = '';
            Object.keys(settings.spoilerValues).forEach(adv => {
                window.MSPFA.request(0, {
                    do: "story",
                    s: adv
                }, story => {
                    if (typeof story !== "undefined") {
                        const storyTr = listTbody.insertRow(listTable.rows);
                        const titleLink = document.createElement('a');
                        Object.assign(titleLink, { className: 'major', href: `/my/stories/pages/?s=${adv}&click=s`, textContent: story.n, target: '_blank' });
                        storyTr.insertCell(0).appendChild(titleLink);
                        const deleteButton = document.createElement('input');
                        Object.assign(deleteButton, { className: 'major', type: 'button', value: 'Delete' });
                        storyTr.insertCell(1).appendChild(deleteButton);

                        deleteButton.addEventListener('click', () => {
                            setTimeout(() => {
                                window.MSPFA.dialog('Delete adventure spoilers?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
                                    if (output === "Yes") {
                                        delete settings.spoilerValues[adv];
                                        saveData(settings);

                                        setTimeout(() => {
                                            spoilerButton.click();
                                        }, 1);

                                        if (Object.keys(settings.spoilerValues).length === 0) {
                                            spoilerButton.disabled = true;
                                        }
                                    }
                                });
                            }, 1);
                        });
                    }
                });
            });
            window.MSPFA.dialog('Manage Spoiler Values', spoilerMsg, ["Delete All", "Close"], (output, form) => {
                if (output === "Delete All") {
                    setTimeout(() => {
                        window.MSPFA.dialog('Delete all Spoiler Values?', 'Are you sure you want to delete all spoiler values?\nThis action cannot be undone!', ["Yes", "No"], (output, form) => {
                            if (output === "Yes") {
                                settings.spoilerValues = {};
                                saveData(settings);
                                spoilerButton.disabled = true;
                            }
                        });
                    }, 1);
                }
            });
        });

        // Add event listeners
        plusTbody.querySelectorAll('input, select').forEach(elm => {
            elm.addEventListener("change", () => {
                saveBtn.disabled = false;
            });
        });

        saveBtn.addEventListener('mouseup', () => {
            settings.autospoiler = spoilerInput.checked;
            settings.style = cssSelect.selectedIndex;
            settings.styleURL = customCssInput.value;
            settings.auto502 = errorInput.checked;
            settings.textFix = textFixInput.checked;
            settings.pixelFix = pixelFixInput.checked;
            settings.dialogKeys = dialogKeysInput.checked;
            settings.dialogFocus = dialogFocusInput.checked;
            settings.commandScroll = commandScrollInput.checked;
            settings.preload = preloadInput.checked;
            settings.night = false;
            console.log(settings);
            saveData(settings);

            updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);

            extraStyle.textContent = pixelText() + ' *{transition:1s}';

            extraStyle.textContent = pixelText();
            setTimeout(() => {
                    extraStyle.textContent = pixelText();
            }, 1000);
        });
    }
    else if (location.pathname === "/my/messages/") { // New buttons
        // Select all read messages button.
        const selRead = document.createElement('input');
        Object.assign(selRead, { value: 'Select Read', className: 'major', type: 'button' });

        // On click, select all messages with the style attribute indicating it as read.
        selRead.addEventListener('mouseup', () => {
            document.querySelectorAll('td[style="border-left: 8px solid rgb(221, 221, 221);"] > input').forEach((m) => m.click());
        });

        // Select duplicate message (multiple update notifications).
        const selDupe = document.createElement('input');
        Object.assign(selDupe, { value: 'Select Same', className: 'major', type: 'button', style: 'margin-top: 6px' });

        selDupe.addEventListener('mouseup', evt => {
            const temp = document.querySelectorAll('#messages > tr');
            const msgs = [];
            for (let i = temp.length - 1; i >= 0; i--) {
                msgs.push(temp[i]);
            }
            const titles = [];
            msgs.forEach((msg) => {
                const title = msg.querySelector('a.major').textContent;
                // Select only adventure updates
                if (/^New update: /.test(title)) {
                    if (titles.indexOf(title) === -1) {
                        if (msg.querySelector('td').style.cssText !== "border-left: 8px solid rgb(221, 221, 221);") {
                            titles.push(title);
                        }
                    } else {
                        msg.querySelector('input').click();
                    }
                }
            });
        });

        // Prune button
        const pruneButton = document.createElement('input');
        Object.assign(pruneButton, { type: 'button', value: 'Prune', className: 'major' });

        pruneButton.addEventListener('click', () => {
            const ageInput = document.createElement('input');
            Object.assign(ageInput, { type: 'number', min: 1, max: 10, value: 1 });

            const msgState = document.createElement('select');
            ['all', 'all unread', 'all read'].forEach(option => {
                const op = document.createElement('option');
                op.textContent = option;
                msgState.appendChild(op);
            });

            const timeUnit = document.createElement('select');
            ['month(s)', 'week(s)', 'day(s)'].forEach(option => {
                const op = document.createElement('option');
                op.textContent = option;
                timeUnit.appendChild(op);
            });
            timeUnit.childNodes[1].setAttribute('selected', 'selected');

            const msg = document.createElement('span');
            msg.appendChild(document.createTextNode('Prune '));
            msg.appendChild(msgState);
            msg.appendChild(document.createTextNode(' messages older than '));
            msg.appendChild(ageInput);
            msg.appendChild(timeUnit);

            window.MSPFA.dialog('Prune messages', msg, ['Prune', 'Cancel'], (output, form) => {
                if (output === 'Prune') {
                    document.querySelector('#messages').childNodes.forEach(node => {
                        if (node.firstChild.firstChild.checked) {
                            node.firstChild.firstChild.click();
                        }

                        const selectedState = msgState.selectedOptions[0].textContent;
                        const selectedUnit = timeUnit.selectedOptions[0].textContent;

                        if (selectedState === 'all unread') {
                            if (node.firstChild.style.borderLeftColor === 'rgb(221, 221, 221)') {
                                return;
                            }
                        }
                        else if (selectedState === 'all read') {
                            if (node.firstChild.style.borderLeftColor === 'rgb(92, 174, 223)') {
                                return;
                            }
                        }
                        const dateText = node.childNodes[2].childNodes[2].textContent.split(' - ');
                        const messageDate = new Date(dateText[dateText.length-1]);
                        const currentDate = Date.now();
                        const diff = Math.floor(Math.round((currentDate-messageDate)/(1000*60*60))/24); // Difference in days

                        if (selectedUnit === 'month(s)') diff = Math.floor(diff / 30);
                        else if (selectedUnit === 'week(s)') diff = Math.floor(diff / 7);

                        if (diff >= ageInput.value) {
                            node.firstChild.firstChild.click();
                        }
                    });

                    setTimeout(() => {
                        document.querySelector('input[value=Delete]').click();
                    }, 1);
                }
            });
        });

        // Maybe add a "Merge Updates" button?
        // [Merge Updates] would create a list of updates, similar to [Select Same]

        // Add buttons to the page.
        const del = document.querySelector('#deletemsgs');
        del.parentNode.appendChild(newBr());
        del.parentNode.appendChild(selRead);
        del.parentNode.appendChild(document.createTextNode(' '));
        del.parentNode.appendChild(selDupe);
        del.parentNode.appendChild(document.createTextNode(' '));
        del.parentNode.appendChild(pruneButton);

        // Click the green cube to open the update/comment in a new tab, and mark notification as read.
        pageLoad(() => {
            if (document.querySelector('#messages').childNodes.length > 0) {
                if (document.querySelector('#messages').textContent === 'No new messages were found.') {
                    // Disable some buttons if there are no messages.
                    pruneButton.disabled = true;
                    selDupe.disabled = true;
                    return true;
                } else {
                    document.querySelector('#messages').childNodes.forEach(node => {
                        if (node.textContent.includes('New update:') && node.textContent.includes('MS Paint Fan Adventures')) {
                            const link = addLink(node.querySelector('.cellicon'), node.querySelector('.spoiler a').href);
                            link.addEventListener('mouseup', () => {
                                const spoiler = node.querySelector('.spoiler');
                                const button = spoiler.querySelector('input');
                                spoiler.className = 'spoiler closed';
                                button.click();
                                button.click();
                            });
                        }
                        else if (node.textContent.includes('New comment on ') && node.textContent.includes('MS Paint Fan Adventures')) {
                            const link = addLink(node.querySelector('.cellicon'), node.querySelectorAll('.spoiler a')[1].href + '#commentbox');
                            link.addEventListener('mouseup', () => {
                                const spoiler = node.querySelector('.spoiler');
                                const button = spoiler.querySelector('input');
                                spoiler.className = 'spoiler closed';
                                button.click();
                                button.click();
                            });
                        }
                    });
                    return true;
                }
            }
        });
    }
    else if (location.pathname === "/my/messages/new/" && location.search) { // Auto-fill user when linked from a user page
        const recipientInput = document.querySelector('#addrecipient');
        recipientInput.value = params.u;
        pageLoad(() => {
            const recipientButton = document.querySelector('#addrecipientbtn');
            if (recipientButton) {
                recipientButton.click();
                if (recipientInput.value === "") { // If the button press doesn't work
                    return true;
                }
            }
        });
    }
    else if (location.pathname === "/my/stories/") {
        // Add links to buttons
        pageLoad(() => {
            const adventures = document.querySelectorAll('#stories tr');
            if (adventures.length > 0) {
                adventures.forEach(story => {
                    const buttons = story.querySelectorAll('input.major');
                    const id = story.querySelector('a').href.replace('https://mspfa.com/', '').replace('&p=1', '');
                    if (id) {
                        addLink(buttons[0], `/my/stories/info/${id}`);
                        addLink(buttons[1], `/my/stories/pages/${id}`);
                        addLink(story.querySelector('img'), `/${id}&p=1`);
                    }
                });
                return true;
            }
            if (pageLoaded) return true;
        });

        // Add user guides
        const guides = ["A Guide To Uploading Your Comic To MSPFA", "MSPFA Etiquette", "Fanventure Guide for Dummies", "CSS Guide", "HTML and CSS Things", ];
        const links = ["https://docs.google.com/document/d/17QI6Cv_BMbr8l06RrRzysoRjASJ-ruWioEtVZfzvBzU/edit?usp=sharing", "/?s=27631", "/?s=29299", "/?s=21099", "/?s=23711"];
        const authors = ["Farfrom Tile", "Radical Dude 42", "nzar", "MadCreativity", "seymour schlong"];

        const parentTd = document.querySelector('.container > tbody > tr:last-child > td');
        const unofficial = parentTd.querySelector('span');
        unofficial.textContent = "Unofficial Guides";
        const guideTable = document.createElement('table');
        const guideTbody = document.createElement('tbody');
        guideTable.style.width = "100%";
        guideTable.style.textAlign = "center";

        guideTable.appendChild(guideTbody);
        parentTd.appendChild(guideTable);

        for (let i = 0; i < guides.length; i++) {
            const guideTr = guideTbody.insertRow(i);
            const guideTd = guideTr.insertCell(0);
            const guideLink = document.createElement('a');
            Object.assign(guideLink, { href: links[i], textContent: guides[i], className: 'major' });
            guideTd.appendChild(guideLink);
            guideTd.appendChild(newBr());
            guideTd.appendChild(document.createTextNode('by '+authors[i]));
            guideTd.appendChild(newBr());
            guideTd.appendChild(newBr());
        }
    }
    else if (location.pathname === "/my/stories/info/" && location.search) {
        // Button links
        addLink(document.querySelector('#userfavs'), `/readers/?s=${params.s}`);
        addLink(document.querySelector('#editpages'), `/my/stories/pages/?s=${params.s}`);

        // Download adventure data
        if (params.s !== 'new') {
            const downloadButton = document.createElement('input');
            Object.assign(downloadButton, { className: 'major', value: 'Export Data', type: 'button', style: 'margin-top: 6px' });
            const downloadLink = document.createElement('a');
            window.MSPFA.request(0, {
                do: "story",
                s: params.s
            }, (s) => {
                if (s) {
                    downloadLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(s, null, 4)));
                }
            });
            downloadLink.setAttribute('download', `${params.s}.json`);
            downloadLink.appendChild(downloadButton);
            document.querySelector('#savestory').parentNode.appendChild(newBr());
            document.querySelector('#savestory').parentNode.appendChild(downloadLink);
        }
    }
    else if (location.pathname === "/my/stories/pages/" && location.search) {
        const adventureID = params.s;

        const notifyLabel = createLabel('Notify readers of new pages during this editing session: ', 'notifyreaders');
        const notifyButton = document.querySelector('#notifyreaders');
        notifyButton.previousSibling.textContent = '';
        notifyButton.parentNode.insertBefore(notifyLabel, notifyButton);/**/

        if (!drafts[adventureID]) {
            drafts[adventureID] = {};
            saveDrafts(drafts);
        }

        pageLoad(() => {
            if (document.querySelector('#storyname').textContent !== '-') {
                drafts[adventureID].cachedTitle = document.querySelector('#storyname').textContent;
                saveDrafts(drafts);
                return true;
            }
        });

        // Button links
        addLink(document.querySelector('#editinfo'), `/my/stories/info/?s=${adventureID}`);

        // Default spoiler values
        const replaceButton = document.querySelector('#replaceall');
        const spoilerButton = document.createElement('input');
        Object.assign(spoilerButton, { className: 'major', value: 'Default Spoiler Values', type: 'button'});
        replaceButton.parentNode.insertBefore(spoilerButton, replaceButton);
        replaceButton.parentNode.insertBefore(newBr(), replaceButton);
        replaceButton.parentNode.insertBefore(newBr(), replaceButton);

        if (!settings.spoilerValues[adventureID]) {
            settings.spoilerValues[adventureID] = {
                open: 'Show',
                close: 'Hide'
            }
        }

        spoilerButton.addEventListener('click', evt => {
            const spoilerSpan = document.createElement('span');
            const spoilerOpen = document.createElement('input');
            const spoilerClose = document.createElement('input');
            spoilerSpan.appendChild(document.createTextNode('Open button text:'));
            spoilerSpan.appendChild(newBr());
            spoilerSpan.appendChild(spoilerOpen);
            spoilerSpan.appendChild(newBr());
            spoilerSpan.appendChild(newBr());
            spoilerSpan.appendChild(document.createTextNode('Close button text:'));
            spoilerSpan.appendChild(newBr());
            spoilerSpan.appendChild(spoilerClose);

            spoilerOpen.value = settings.spoilerValues[adventureID].open;
            spoilerClose.value = settings.spoilerValues[adventureID].close;

            window.MSPFA.dialog('Default Spoiler Values', spoilerSpan, ['Save', 'Cancel'], (output, form) => {
                if (output === 'Save') {
                    settings.spoilerValues[adventureID].open = spoilerOpen.value === '' ? 'Show' : spoilerOpen.value;
                    settings.spoilerValues[adventureID].close = spoilerClose.value === '' ? 'Hide' : spoilerClose.value;
                    if (settings.spoilerValues[adventureID].open === 'Show' && settings.spoilerValues[adventureID].close === 'Hide') {
                        delete settings.spoilerValues[adventureID];
                    }
                    saveData(settings);
                }
            });
        });

        document.querySelector('input[title="Spoiler"]').addEventListener('click', evt => {
            document.querySelector('#dialog input[name="open"]').value = document.querySelector('#dialog input[name="open"]').placeholder = settings.spoilerValues[adventureID].open;
            document.querySelector('#dialog input[name="close"]').value = document.querySelector('#dialog input[name="close"]').placeholder = settings.spoilerValues[adventureID].close;
        });

        // --- Custom BBToolbar buttons
        // Buttonless spoilers
        const flashButton = document.querySelector('input[title=Flash]');
        const newSpoilerButton = document.createElement('input');
        newSpoilerButton.setAttribute('data-tag', 'Buttonless Spoiler');
        Object.assign(newSpoilerButton, { title: 'Buttonless Spoiler', type: 'button', style: 'background-position: -66px -88px;' });

        newSpoilerButton.addEventListener('click', evt => {
            const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
            if (bbe) {
                bbe.focus();
                const start = bbe.selectionStart;
                const end = bbe.selectionEnd;
                bbe.value = bbe.value.slice(0, start) + '<div class="spoiler"><div>' + bbe.value.slice(start, end) + '</div></div>' + bbe.value.slice(end);
                bbe.selectionStart = start + 26;
                bbe.selectionEnd = end + 26;
            }
        });

        flashButton.parentNode.insertBefore(newSpoilerButton, flashButton);

        // Audio button
        const audioButton = document.createElement('input');
        Object.assign(audioButton, { title: 'Audio Player', type: 'button', style: 'background-position: -22px -110px' });

        audioButton.addEventListener('click', evt => {
            const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
            if (bbe) {
                const msg = window.MSPFA.parseBBCode('Audio URL:<br>');
                const audioInput = document.createElement('input');
                Object.assign(audioInput, { type: 'url', name: 'audio-url', required: true });

                const autoplayButton = document.createElement('input');
                autoplayButton.type = 'checkbox';
                autoplayButton.id = 'autoplay';
                autoplayButton.checked = true;

                const loopButton = document.createElement('input');
                loopButton.type = 'checkbox';
                loopButton.id = 'loop';
                loopButton.checked = true;

                const controlsButton = document.createElement('input');
                controlsButton.type = 'checkbox';
                controlsButton.id = 'controls';

                msg.appendChild(audioInput);
                msg.appendChild(newBr());
                msg.appendChild(createLabel('Autoplay: ', 'autoplay'));
                msg.appendChild(autoplayButton);
                msg.appendChild(newBr());
                msg.appendChild(createLabel('Loop: ', 'loop'));
                msg.appendChild(loopButton);
                msg.appendChild(newBr());
                msg.appendChild(createLabel('Show controls: ', 'controls'));
                msg.appendChild(controlsButton);
                msg.appendChild(newBr());

                window.MSPFA.dialog("Audio Player", msg, ["Okay", "Cancel"], (output, form) => {
                    if (output == "Okay") {
                        bbe.focus();
                        const start = bbe.selectionStart;
                        const end = bbe.selectionEnd;
                        const properties = `"${autoplayButton.checked ? ' autoplay' : ''}${loopButton.checked ? ' loop' : ''}${controlsButton.checked ? ' controls' : ''}`;
                        bbe.value = bbe.value.slice(0, start) + '<audio src="' + audioInput.value + properties +'>' + bbe.value.slice(start);
                        bbe.selectionStart = start + properties.length + audioInput.value.length + 13;
                        bbe.selectionEnd = end + properties.length + audioInput.value.length + 13;
                    }

                });

                audioInput.select();
            }
        });

        flashButton.insertAdjacentElement('afterEnd', audioButton);

        // YouTube button
        const youtubeButton = document.createElement('input');
        Object.assign(youtubeButton, { title: 'YouTube Video', type: 'button', style: 'background-position: 0px -110px' });

        youtubeButton.addEventListener('click', evt => {
            const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
            if (bbe) {
                const msg = window.MSPFA.parseBBCode('Video URL:<br>');
                const videoUrl = document.createElement('input');
                videoUrl.type = 'url';
                videoUrl.name = 'youtube';
                videoUrl.required = true;

                const autoplayButton = document.createElement('input');
                autoplayButton.type = 'checkbox';
                autoplayButton.checked = true;
                autoplayButton.id = 'autoplay';

                const controlsButton = document.createElement('input');
                controlsButton.type = 'checkbox';
                controlsButton.checked = true;
                controlsButton.id = 'controls';

                const fullscreenButton = document.createElement('input');
                fullscreenButton.type = 'checkbox';
                fullscreenButton.checked = true;
                fullscreenButton.id = 'fullscreen';

                const widthInput = document.createElement('input');
                Object.assign(widthInput, { type: 'number', required: true, value: 650, style: 'width: 5em' });

                const heightInput = document.createElement('input');
                Object.assign(heightInput, { type: 'number', required: true, value: 450, style: 'width: 5em' });

                msg.appendChild(videoUrl);
                msg.appendChild(newBr());
                msg.appendChild(createLabel('Autoplay: ', 'autoplay'));
                msg.appendChild(autoplayButton);
                msg.appendChild(newBr());
                msg.appendChild(createLabel('Show controls: ', 'controls'));
                msg.appendChild(controlsButton);
                msg.appendChild(newBr());
                msg.appendChild(createLabel('Allow fullscreen: ', 'fullscreen'));
                msg.appendChild(fullscreenButton);
                msg.appendChild(newBr());
                msg.appendChild(document.createTextNode('Embed size: '));
                msg.appendChild(widthInput);
                msg.appendChild(document.createTextNode('x'));
                msg.appendChild(heightInput);

                window.MSPFA.dialog("YouTube Embed", msg, ["Okay", "Cancel"], (output, form) => {
                    if (output == "Okay") {
                        let videoID = videoUrl.value.split('/');
                        videoID = videoID[videoID.length-1].replace('watch?v=', '').split('&')[0];

                        bbe.focus();
                        const start = bbe.selectionStart;
                        const end = bbe.selectionEnd;
                        const iframeContent = `<iframe width="${widthInput.value}" height="${heightInput.value}" src="https://www.youtube.com/embed/${videoID}?autoplay=${+autoplayButton.checked}&controls=${+controlsButton.checked}" frameborder="0" allow="accelerometer; ${autoplayButton.checked ? 'autoplay; ' : ''}encrypted-media;"${fullscreenButton.checked ? ' allowfullscreen' : ''}></iframe>`;
                        bbe.value = bbe.value.slice(0, start) + iframeContent + bbe.value.slice(start);
                        bbe.selectionStart = start + iframeContent + 13;
                        bbe.selectionEnd = end + iframeContent + 13;
                    }

                });

                videoUrl.select();
            }
        });

        flashButton.insertAdjacentElement('afterEnd', youtubeButton);
        flashButton.insertAdjacentText('afterEnd', ' ');

        // Get preview link
        const getPreviewLink = (form) => {
            const page = parseInt(form.querySelector('a.major').textContent.replace('Page ', ''));
            return "/preview/?s=" + params.s + "&p=" + page + "&d=" + encodeURIComponent(JSON.stringify({
                p: page,
                c: form.querySelector('input[name=cmd]').value,
                b: form.querySelector('textarea[name=body]').value,
                n: form.querySelector('input[name=next]').value,
                k: !form.querySelector('input[name=usekeys]').checked
            }));
        }

        // -- Drafts --
        // Accessing draft text
        const accessDraftsButton = document.createElement('input');
        Object.assign(accessDraftsButton, { className: 'major', value: 'Saved Drafts', type: 'button' });
        replaceButton.parentNode.insertBefore(accessDraftsButton, replaceButton);
        accessDraftsButton.parentNode.insertBefore(document.createTextNode(' '), replaceButton);
        //accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton);
        //accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton);

        accessDraftsButton.addEventListener('click', () => {
            loadDrafts();

            const draftDialog = window.MSPFA.parseBBCode('Use the textbox below to copy out the data and save to a file somewhere else, or click the download button below.\nYou can also paste in data to replace the current drafts to ones stored there.');
            const draftInputTextarea = document.createElement('textarea');
            draftInputTextarea.placeholder = 'Paste your draft data here';
            draftInputTextarea.style = 'width: 100%; box-sizing: border-box; resize: vertical;';

            const downloadLink = document.createElement('a');
            downloadLink.textContent = 'Download drafts';
            downloadLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(drafts[adventureID], null, 4)));
            downloadLink.setAttribute('download', `${adventureID}.json`);

            draftInputTextarea.rows = 8;
            draftDialog.appendChild(newBr());
            draftDialog.appendChild(newBr());
            draftDialog.appendChild(draftInputTextarea);
            draftDialog.appendChild(newBr());
            draftDialog.appendChild(newBr());
            draftDialog.appendChild(downloadLink);
            setTimeout(() => {
                draftInputTextarea.focus();
                draftInputTextarea.selectionStart = 0;
                draftInputTextarea.selectionEnd = 0;
                draftInputTextarea.scrollTop = 0;
            }, 1);

            draftInputTextarea.value = JSON.stringify(drafts[adventureID], null, 4);

            window.MSPFA.dialog('Saved Drafts', draftDialog, ["Load Draft", "Cancel"], (output, form) => {
                if (output === "Load Draft") {
                    if (draftInputTextarea.value === '') {
                        setTimeout(() => {
                            window.MSPFA.dialog('Saved Drafts', window.MSPFA.parseBBCode('Are you sure you want to delete this adventure\'s draft data?\nMake sure you have it saved somewhere!'), ["Delete", "Cancel"], (output, form) => {
                                if (output === "Delete") {
                                    loadDrafts();
                                    drafts[adventureID] = {};

                                    if (settings.drafts && settings.drafts[adventureID]) {
                                        delete settings.drafts[adventureID];
                                        saveData(settings);
                                    }

                                    saveDrafts(drafts);
                                }
                            });
                        }, 1);
                    } else if (draftInputTextarea.value !== JSON.stringify(drafts[adventureID], null, 4)) {
                        setTimeout(() => {
                            window.MSPFA.dialog('Saved Drafts', window.MSPFA.parseBBCode('Are you sure you want to load this draft data?\nAll previous draft data for this adventure will be lost!'), ["Load", "Cancel"], (output, form) => {
                                if (output === "Load") {
                                    let newData = {};
                                    try { // Just in case the data given is invalid.
                                        newData = JSON.parse(draftInputTextarea.value);
                                    } catch (err) {
                                        console.error(err);
                                        setTimeout(() => {
                                            window.MSPFA.dialog('Error', window.MSPFA.parseBBCode('The entered data is invalid.'), ["Okay"]);
                                        }, 1);
                                        return;
                                    }

                                    loadDrafts();
                                    drafts[adventureID] = newData;
                                    saveDrafts(drafts);
                                }
                            });
                        }, 1);
                    }
                }
            });
        });

        const addDraftPagesButton = document.createElement('input');
        replaceButton.parentNode.insertBefore(addDraftPagesButton, replaceButton);
        addDraftPagesButton.parentNode.insertBefore(newBr(), replaceButton);
        addDraftPagesButton.parentNode.insertBefore(newBr(), replaceButton);

        Object.assign(addDraftPagesButton, { className: 'major', value: 'Add Draft Pages', type: 'button' });
        addDraftPagesButton.addEventListener('click', () => {
            const newPageForm = document.querySelector('#newpage');
            let currentPage = 1;
            let pageAmount = 0;
            if (newPageForm.nextSibling.nodeName !== '#text') {
                currentPage = parseInt(newPageForm.nextSibling.id.replace('p', ''))+1;
            }
            for (let i = currentPage; drafts[adventureID][i] && pageAmount < 10; i++) {
                pageAmount++;
            }

            if (pageAmount > 0) {
                window.MSPFA.dialog("Drafts: Add New Pages", document.createTextNode(`This will add the pages with draft data from pages ${currentPage}-${currentPage+pageAmount-1} (you are only allowed 10 at once)`), ["Okay", "Cancel"], (output, form) => {
                    if (output === "Okay") {
                        for (let i = currentPage; drafts[adventureID][i] && i < currentPage+10; i++) {
                            newPageForm.querySelector('input[name=cmd]').value = drafts[adventureID][i].command;
                            newPageForm.querySelector('textarea[name=body]').value = drafts[adventureID][i].pageContent;
                            if (drafts[adventureID][i].next) newPageForm.querySelector('input[name=next]').value = drafts[adventureID][i].next;

                            newPageForm.querySelector('input[name=save]').click();
                        }
                    }
                });
            }
        });

        // Draft stuff
        const showDraftDialog = (pageNum) => {
            loadDrafts();

            const msg = document.createElement('span');
            msg.appendChild(document.createTextNode('Command:'));
            msg.appendChild(document.createElement('br'));

            const commandInput = document.createElement('input');
            Object.assign(commandInput, { style: 'width: 100%; box-sizing: border-box;', readOnly: true, });

            msg.appendChild(commandInput);
            msg.appendChild(document.createElement('br'));
            msg.appendChild(document.createElement('br'));

            msg.appendChild(document.createTextNode('Body:'));

            const bodyInput = document.createElement('textarea');
            Object.assign(bodyInput, { style: 'width: 100%; box-sizing: border-box; resize: vertical;', readOnly: true, rows: 8 });

            msg.appendChild(bodyInput);

            const pageElement = document.querySelector(`#p${pageNum}`);

            let shownMessage = msg;
            let optionButtons = [];

            const commandElement = pageElement.querySelector('input[name="cmd"]');
            const pageContentElement = pageElement.querySelector('textarea[name="body"]');

            if (typeof drafts[adventureID][pageNum] === "undefined") {
                shownMessage = document.createTextNode('There is no draft saved for this page.');
                optionButtons = ["Save New", "Close"];
            } else {
                commandInput.value = drafts[adventureID][pageNum].command;
                bodyInput.textContent = drafts[adventureID][pageNum].pageContent;
                optionButtons = ["Save New", "Load", "Delete", "Close"];
            }

            window.MSPFA.dialog(`Page ${pageNum} Draft`, shownMessage, optionButtons, (output, form) => {
                if (output === "Save New") {
                    if (typeof drafts[adventureID][pageNum] === "undefined") {
                        loadDrafts();
                        drafts[adventureID][pageNum] = {
                            command: commandElement.value,
                            pageContent: pageContentElement.value
                        }
                        saveDrafts(drafts);
                    } else {
                        setTimeout(() => {
                            window.MSPFA.dialog('Overwrite current draft?', document.createTextNode('Doing this will overwrite your current draft with what is currently written in the page box. Are you sure?'), ["Yes", "No"], (output, form) => {
                                if (output === "Yes") {
                                    loadDrafts();
                                    drafts[adventureID][pageNum] = {
                                        command: commandElement.value,
                                        pageContent: pageContentElement.value
                                    }
                                    saveDrafts(drafts);
                                }
                            });
                        }, 1);
                    }
                } else if (output === "Load") {
                    if (pageContentElement.value === '' && (commandElement.value === '' || commandElement.value === document.querySelector('#defaultcmd').value)) {
                        commandElement.value = drafts[adventureID][pageNum].command;
                        pageContentElement.value = drafts[adventureID][pageNum].pageContent;
                        pageElement.querySelector('input[value="Save"]').disabled = false;
                    } else {
                        setTimeout(() => {
                            window.MSPFA.dialog('Overwrite current page?', document.createTextNode('Doing this will overwrite the page\'s content with what is currently written in the draft. Are you sure?'), ["Yes", "No"], (output, form) => {
                                if (output === "Yes") {
                                    commandElement.value = drafts[adventureID][pageNum].command;
                                    pageContentElement.value = drafts[adventureID][pageNum].pageContent;
                                    pageElement.querySelector('input[value="Save"]').disabled = false;
                                }
                            });
                        }, 1);
                    }
                } else if (output === "Delete") {
                    setTimeout(() => {
                        window.MSPFA.dialog('Delete this draft?', document.createTextNode('This action is irreversable! Are you sure?'), ["Yes", "No"], (output, form) => {
                            if (output === "Yes") {
                                loadDrafts();
                                delete drafts[adventureID][pageNum];

                                if (settings.drafts && settings.drafts[adventureID] && settings.drafts[adventureID][pageNum]) {
                                    delete settings.drafts[adventureID][pageNum];
                                    saveData(settings);
                                }

                                saveDrafts(drafts);
                            }
                        });
                    }, 1);
                }
            });
        }

        const createDraftButton = (form) => {
            const draftButton = document.createElement('input');
            Object.assign(draftButton, { className: 'major draft', type: 'button', value: 'Draft' });
            draftButton.addEventListener('click', () => {
                showDraftDialog(form.id.replace('p', ''));
            });
            return draftButton;
        }

        pageLoad(() => {
            let allPages = document.querySelectorAll('#storypages form:not(#newpage)');
            if (allPages.length !== 0) {
                allPages.forEach(form => {
                    const prevButton = form.querySelector('input[name="preview"]');
                    prevButton.parentNode.insertBefore(createDraftButton(form), prevButton);
                    prevButton.parentNode.insertBefore(document.createTextNode(' '), prevButton);

                    // Preview
                    const previewButton = form.querySelector('input[value=Preview]');
                    const previewLink = addLink(previewButton, getPreviewLink(form), '_blank');
                    previewButton.addEventListener('mousedown', () => {
                        previewLink.href = getPreviewLink(form);
                    });

                    // "Enable keyboard shortcuts" label
                    const shortcutCheck = form.querySelector('input[type="checkbox"]');
                    shortcutCheck.previousSibling.textContent = '';
                    shortcutCheck.id = `key-${form.id}`;
                    shortcutCheck.parentNode.insertBefore(createLabel('Enable keyboard shortcuts: ', shortcutCheck.id), shortcutCheck);
                });
                document.querySelector('input[value="Add"]').addEventListener('click', () => {
                    allPages = document.querySelectorAll('#storypages form:not(#newpage)');
                    const form = document.querySelector(`#p${allPages.length}`);
                    const prevButton = form.querySelector('input[name="preview"]');
                    prevButton.parentNode.insertBefore(createDraftButton(form), prevButton);
                    prevButton.parentNode.insertBefore(document.createTextNode(' '), prevButton);

                    // Preview link
                    const previewButton = form.querySelector('input[value=Preview]');
                    const previewLink = addLink(previewButton, getPreviewLink(form), '_blank');
                    previewButton.addEventListener('mousedown', () => {
                        previewLink.href = getPreviewLink(form);
                    });

                    // "Enable keyboard shortcuts" label
                    const shortcutCheck = form.querySelector('input[type="checkbox"]');
                    shortcutCheck.previousSibling.textContent = '';
                    shortcutCheck.id = `key-${form.id}`;
                    shortcutCheck.parentNode.insertBefore(createLabel('Enable keyboard shortcuts: ', shortcutCheck.id), shortcutCheck);
                });
                const newForm = document.querySelector('#newpage');
                {
                    // "Enable keyboard shortcuts" label
                    const shortcutCheck = newForm.querySelector('input[type="checkbox"]');
                    shortcutCheck.previousSibling.textContent = '';
                    shortcutCheck.id = `key-${newForm.id}`;
                    shortcutCheck.parentNode.insertBefore(createLabel('Enable keyboard shortcuts: ', shortcutCheck.id), shortcutCheck);
                }
                const newPreviewButton = newForm.querySelector('input[value=Preview]');
                const newPreviewLink = addLink(newPreviewButton, getPreviewLink(newForm), '_blank');
                newPreviewButton.addEventListener('mousedown', () => {
                    newPreviewLink.href = getPreviewLink(newForm);
                });
                return true;
            }
        });

        if (params.click) {
            if (params.click === 's') {
                spoilerButton.click();
            } else if (params.click === 'd') {
                accessDraftsButton.click();
            }
        }

        // Don't scroll after pressing a BBToolbar button (awesome)
        let lastScroll = window.scrollY;
        pageLoad(() => {
            if (document.querySelectorAll('#storypages textarea').length > 1) {
                document.querySelectorAll('#storypages textarea').forEach(textarea => {
                    textarea.addEventListener('focus', () => {
                        window.scrollTo(window.scrollX, lastScroll);
                    });
                });

                document.addEventListener('scroll', evt => {
                    lastScroll = window.scrollY;
                });
                return true;
            }
        });

        // Focus on the text input when clicking on the Color or Background Color BBToolbar buttons
        const colourButtons = [document.querySelector('#bbtoolbar input[data-tag=color]'), document.querySelector('#bbtoolbar input[data-tag=background]')];

        let prevColour;

        colourButtons[0].addEventListener('click', () => {
            const inputs = document.querySelectorAll('#dialog input');
            document.querySelector('button[data-value="Okay"]').addEventListener('click', () => {
                prevColour = inputs[0].value;
            });
            if (prevColour) {
                inputs[0].value = prevColour;
                inputs[1].value = prevColour;
            }
        });

        colourButtons.forEach(button => {
            button.addEventListener('click', () => {
                document.querySelector('#dialog input[type=text]').select();
            });
        });
    }
    else if (location.pathname === "/my/profile/") {
        // Nothing
    }
    else if (location.pathname === "/user/") {
        // Button links
        pageLoad(() => {
            const msgButton = document.querySelector('#sendmsg');
            if (msgButton) {
                addLink(msgButton, `/my/messages/new/?u=${params.u}`);
                addLink(document.querySelector('#favstories'), `/favs/?u=${params.u}`);
                return true;
            }
        });

        // Add extra user stats
        pageLoad(() => {
            if (window.MSPFA) {
                const stats = document.querySelector('#userinfo table');

                const joinTr = stats.insertRow(1);
                const joinTextTd = joinTr.insertCell(0);
                joinTextTd.appendChild(document.createTextNode("Account created:"));
                const joinDate = joinTr.insertCell(1);
                const joinTime = document.createElement('b');
                joinTime.textContent = "Loading...";
                joinDate.appendChild(joinTime);

                const advCountTr = stats.insertRow(2);
                const advTextTd = advCountTr.insertCell(0);
                advTextTd.appendChild(document.createTextNode("Adventures created:"));
                const advCount = advCountTr.insertCell(1);
                const advCountText = document.createElement('b');
                advCountText.textContent = "Loading...";
                advCount.appendChild(advCountText);

                // Show user creation date
                window.MSPFA.request(0, {
                    do: "user",
                    u: params.u
                }, user => {
                    if (typeof user !== "undefined") {
                        joinTime.textContent = new Date(user.d).toString().split(' ').splice(1, 4).join(' ');
                    }

                    // Show created adventures
                    window.MSPFA.request(0, {
                        do: "editor",
                        u: params.u
                    }, s => {
                        if (typeof s !== "undefined") {
                            advCountText.textContent = s.length;
                        }

                        // Show favourites
                        if (document.querySelector('#favstories').style.display !== 'none') {
                            const favCountTr = stats.insertRow(3);
                            const favTextTd = favCountTr.insertCell(0);
                            favTextTd.appendChild(document.createTextNode("Adventures favorited:"));
                            const favCount = favCountTr.insertCell(1);
                            const favCountText = document.createElement('b');
                            favCountText.textContent = "Loading...";
                            window.MSPFA.request(0, {
                                do: "favs",
                                u: params.u
                            }, s => {
                                if (typeof s !== "undefined") {
                                    favCountText.textContent = s.length;
                                }
                            });
                            favCount.appendChild(favCountText);
                        }
                    });
                });

                return true;
            }
        });
    }
    else if (location.pathname === "/favs/" && location.search) {
        const toggleButton = document.createElement('input');
        Object.assign(toggleButton, { className: "major", type: "button", value: "Hide Muted Adventures", title: 'Cycle through muted/unmuted adventures' });
        const buttonRow = document.querySelector('table.container.alt').insertRow(2);
        const actionSpan = document.createElement('span');

        let stories = [];
        // Button links
        pageLoad(() => {
            stories = document.querySelectorAll('#stories tr');
            let favCount = 0;

            if (stories.length > 0) {
                stories.forEach(story => {
                    favCount++;
                    const id = story.querySelector('a').href.replace('https://mspfa.com/', '');
                    pageLoad(() => {
                        if (window.MSPFA.me.i) {
                            addLink(story.querySelector('.edit.major'), `/my/stories/info/${id}`);
                            return true;
                        }
                        if (pageLoaded) return true;
                    });
                    addLink(story.querySelector('.rss.major'), `/rss/${id}`);
                });

                // Fav count
                const username = document.querySelector('#username');
                username.parentNode.appendChild(newBr());
                username.parentNode.appendChild(newBr());
                username.parentNode.appendChild(document.createTextNode(`Favorited adventures: ${favCount}`));

                return true;
            }
            if (pageLoaded) return true;
        });

        pageLoad(() => {
            if (window.MSPFA && window.MSPFA.me) {
                if (window.MSPFA.me.i === params.u) {
                    const cell = buttonRow.insertCell(0);
                    cell.appendChild(toggleButton);
                    cell.appendChild(newBr());
                    cell.appendChild(actionSpan);
                    return true;
                }
            }
            if (pageLoaded) return true;
        });

        let type = 0;

        toggleButton.addEventListener('click', () => {
            type++;
            if (type > 2) type = 0;

            stories.forEach(story => {
                const unmuted = story.querySelector('.notify').className.includes(' lit');
                story.style.display = '';
                if (type === 2 && unmuted || type === 1 && !unmuted) {
                    story.style.display = 'none';
                }
            });

            if (type === 0) {
                // show all
                actionSpan.textContent = '';
            }
            else if (type === 1) {
                // hide muted
                toggleButton.value = 'Hide Unmuted Adventures';
            }
            else {
                // only muted
                toggleButton.value = 'Show All Adventures';
            }
        });
    }
    else if (location.pathname === "/search/" && location.search) {
        // Character and word statistics
        const statTable = document.createElement('table');
        const statTbody = document.createElement('tbody');
        const statTr = statTbody.insertRow(0);
        const charCount = statTr.insertCell(0);
        const wordCount = statTr.insertCell(0);
        const statParentTr = document.querySelector('#pages').parentNode.parentNode.insertRow(2);
        const statParentTd = statParentTr.insertCell(0);

        const statHeaderTr = statTbody.insertRow(0);
        const statHeader = document.createElement('th');
        statHeader.colSpan = '2';

        statHeaderTr.appendChild(statHeader);
        statHeader.textContent = 'Statistics may not be entirely accurate.';

        statTable.style.width = "100%";

        charCount.textContent = "Character count: loading...";
        wordCount.textContent = "Word count: loading...";

        statTable.appendChild(statTbody);
        statParentTd.appendChild(statTable);

        pageLoad(() => {
            if (document.querySelector('#pages br')) {
                const bbc = window.MSPFA.BBC.slice();
                bbc.splice(0, 3);

                window.MSPFA.request(0, {
                    do: "story",
                    s: params.s
                }, story => {
                    if (typeof story !== "undefined") {
                        const pageContent = [];
                        story.p.forEach(p => {
                            pageContent.push(p.c);
                            pageContent.push(p.b);
                        });

                        const storyText = pageContent.join(' ')
                        .replace(/\n/g, ' ')
                        .replace(bbc[0][0], '$1')
                        .replace(bbc[1][0], '$1')
                        .replace(bbc[2][0], '$1')
                        .replace(bbc[3][0], '$1')
                        .replace(bbc[4][0], '$2')
                        .replace(bbc[5][0], '$3')
                        .replace(bbc[6][0], '$3')
                        .replace(bbc[7][0], '$3')
                        .replace(bbc[8][0], '$3')
                        .replace(bbc[9][0], '$3')
                        .replace(bbc[10][0], '$2')
                        .replace(bbc[11][0], '$1')
                        .replace(bbc[12][0], '$3')
                        .replace(bbc[13][0], '$3')
                        .replace(bbc[14][0], '')
                        .replace(bbc[16][0], '$1')
                        .replace(bbc[17][0], '$2 $4 $5')
                        .replace(bbc[18][0], '$2 $4 $5')
                        .replace(bbc[19][0], '')
                        .replace(bbc[20][0], '')
                        .replace(/<(.*?)>/g, '');

                        wordCount.textContent = `Word count: ${storyText.split(/ +/g).length}`;
                        charCount.textContent = `Character count: ${storyText.replace(/ +/g, '').length}`;
                    }
                });
                return true;
            }
        });
    }
    else if (location.pathname === "/stories/" && location.search) {

        // Add a button to hide adventures with a tag
        const tagHide = document.createElement('input');
        Object.assign(tagHide, { id: 'taghide', value: '-', className: 'major', type: 'button', style: 'padding: 0;', title: 'Hide Tag' });

        const tagAdd = document.querySelector('#tagadd');
        tagAdd.parentNode.insertBefore(tagHide, tagAdd.nextSibling);
        tagAdd.parentNode.insertBefore(document.createTextNode(' '), tagAdd.nextSibling);

        const tagselect = document.querySelector('#tagselect');
        const taglist = document.querySelector('textarea[name=tags]');
        tagHide.addEventListener('click', () => {
            let tags = [];
			if(taglist.value) {
				tags = taglist.value.split(",");
			}
			if(tagselect.value && tags.indexOf('-'+tagselect.value) == -1) {
				tags.push('-'+tagselect.value);
			}
			taglist.value = tags.join(",");
			tagselect.options[0].selected = true;
        });

        // Add titles on hover to the [?] and [+] buttons
        document.querySelector('#taghelp').title = 'Tip';
        tagAdd.title = 'Add Tag';

        // Click text to check/uncheck boxes
        ['Ongoing', 'Complete', 'Inactive'].forEach(t => {
            const check = document.querySelector(`input[name="${t.toLowerCase()}"]`);
            check.id = `check_${t.toLowerCase()}`;
            const label = createLabel(' ' + t + ' ', check.id);
            check.nextSibling.textContent = '';
            check.parentNode.insertBefore(label, check.nextSibling);
        });

        const adventureList = document.querySelector('#doit');
        const resultAmount = document.createElement('span');
        adventureList.parentNode.appendChild(resultAmount);

        pageLoad(() => {
            if (window.MSPFA) {
                window.MSPFA.request(0, {
                    do: "stories",
                    n: params.n,
                    t: params.t,
                    h: params.h,
                    o: params.o,
                    p: params.p,
                    m: 20000
                }, (s) => {
                    resultAmount.textContent = `Number of results: ${s.length}`;
                    return true;
                });
                return true;
            }
        },1);

        pageLoad(() => {
            const stories = document.querySelector('#stories');
            if (stories.childNodes.length > 0) {
                if (params.load && stories.childNodes.length === 1) {
                    stories.querySelector('a').click();
                }

                stories.querySelectorAll('tr').forEach(story => {
                    const storyID = story.querySelector('a.major').href.split('&')[0].replace(/\D/g, '');
                    addLink(story.querySelector('.rss'), `/rss/?s=${storyID}`);

                    pageLoad(() => {
                        if (window.MSPFA.me.i) {
                            addLink(story.querySelector('.edit.major'), `/my/stories/info/?s=${storyID}`);
                            return true;
                        }
                        if (pageLoaded) return true;
                    });
                });
                return true;
            }
            if (pageLoaded) return true;
        });
    }
})();