Greasy Fork is available in English.

FA Webcomic Autoloader

Gives you the option to load all the subsequent comic pages on a FurAffinity comic page automatically. Even for pages without given Links

// ==UserScript==
// @name        FA Webcomic Autoloader
// @namespace   Violentmonkey Scripts
// @match       *://*.furaffinity.net/*
// @require     https://update.greasyfork.org/scripts/475041/1267274/Furaffinity-Custom-Settings.js
// @require     https://update.greasyfork.org/scripts/483952/1478384/Furaffinity-Request-Helper.js
// @require     https://update.greasyfork.org/scripts/485153/1316289/Furaffinity-Loading-Animations.js
// @require     https://update.greasyfork.org/scripts/485827/1318253/Furaffinity-Match-List.js
// @grant       none
// @version     2.0.6
// @author      Midori Dragon
// @description Gives you the option to load all the subsequent comic pages on a FurAffinity comic page automatically. Even for pages without given Links
// @icon        https://www.furaffinity.net/themes/beta/img/banners/fa_logo.png?v2
// @homepageURL https://greasyfork.org/de/scripts/457759-furaffinity-webcomic-autoloader-2-0
// @supportURL  https://greasyfork.org/de/scripts/457759-furaffinity-webcomic-autoloader-2-0/feedback
// @license     MIT
// ==/UserScript==

// jshint esversion: 8

CustomSettings.name = "Extension Settings";
CustomSettings.provider = "Midori's Script Settings";
CustomSettings.headerName = `${GM_info.script.name} Settings`;
const showSearchButtonSetting = CustomSettings.newSetting("Simular Search Button", "Sets wether the search for simular Pages button is show.", SettingTypes.Boolean, "Show Search Button", true);
const loadingSpinSpeedSetting = CustomSettings.newSetting("Loading Animation", "Sets the duration that the loading animation takes for a full rotation in milliseconds.", SettingTypes.Number, "", 600);
const backwardSearchSetting = CustomSettings.newSetting("Backward Search", "Sets the amount of simular pages to search backward. (More Pages take longer)", SettingTypes.Number, "Backward Search Amount", 2);
CustomSettings.loadSettings();

const matchList = new MatchList(CustomSettings);
matchList.matches = ['net/view'];
if (!matchList.hasMatch())
    return;

const nextText = "next";
const prevText = "prev";
const firstText = "first";

const requestHelper = new FARequestHelper(2);

let lightboxPresent = false;
let currLightboxNo = -1;
let imgCount = 1;

let rootSubmissionImg = document.getElementById("submissionImg");
rootSubmissionImg.setAttribute('imgno', 0);
rootSubmissionImg.setAttribute('rootSubmissionImg', true);
rootSubmissionImg.addEventListener('click', submissionImgOnClick);
let openedSids = [getIdFromUrl(window.location.toString())];

createLoaderButton();

function createLoaderButton() {
    const hasSecondPage = getNavigationIds(document).next;

    const autoLoaderButton = document.createElement('button');
    autoLoaderButton.id = "autoloaderbutton";
    autoLoaderButton.className = "button standard mobile-fix";
    autoLoaderButton.type = "button";
    autoLoaderButton.style.marginTop = "10px";
    autoLoaderButton.style.marginBottom = "20px";

    if (hasSecondPage) {
        autoLoaderButton.textContent = "Enable Comic Autoloader";
        autoLoaderButton.onclick = startAutoloader;
        insertAfter(autoLoaderButton, rootSubmissionImg);
        insertBreakBefore(autoLoaderButton, rootSubmissionImg);
    } else if (showSearchButtonSetting.value) {
        autoLoaderButton.textContent = "Search for simular Pages";
        autoLoaderButton.onclick = startSimularSearch;
        insertAfter(autoLoaderButton, rootSubmissionImg);
        insertBreakBefore(autoLoaderButton, rootSubmissionImg);
    }
}

async function startAutoloader() {
    const autoLoaderButton = document.getElementById("autoloaderbutton");
    autoLoaderButton.parentNode.removeChild(autoLoaderButton);

    let sids = getNavigationIds(document);
    let lastSubmissionImg = document.getElementById("submissionImg");
    while (sids.next) {
        const newDoc = await loadPage(sids.next, lastSubmissionImg);
        lastSubmissionImg = document.getElementById("columnpage").querySelector('img[imgno="' + (openedSids.length - 1) + '"]');
        sids = getNavigationIds(newDoc);
    }
}

async function startSimularSearch() {
    const autoLoaderButton = document.getElementById("autoloaderbutton");
    const spinner = new LoadingTextSpinner(autoLoaderButton);
    spinner.delay = loadingSpinSpeedSetting.value;
    spinner.visible = true;
    const result = await searchAllSimularPages();
    spinner.visible = false;
    if (result)
        autoLoaderButton.parentNode.removeChild(autoLoaderButton);
    else
        autoLoaderButton.textContent = "Nothing found... Search again";
}

function getNavigationIds(doc) {
    let nextSid;
    let prevSid;
    let startSid;
    if (doc) {
        const links = doc.querySelectorAll('a[href]:not([class*="button standard mobile-fix"]), :not([class])');
        for (const elem of links) {
            const navText = elem.textContent.toLowerCase();
            if (navText.length > 12)
                continue;
            if (navText.includes(nextText))
                nextSid = getIdFromUrl(elem.href);
            if (navText.includes(prevText))
                prevSid = getIdFromUrl(elem.href);
            if (navText.includes(firstText))
                startSid = getIdFromUrl(elem.href);
        }
    }
    const sids = { next: nextSid, prev: prevSid, start: startSid };
    return sids;
}

async function loadPage(sid, lastSubmissionImg) {
    if (sid && !openedSids.includes(sid)) {
        const submissionPage = await requestHelper.SubmissionRequests.getSubmissionPage(sid);
        if (submissionPage && submissionPage.getElementById("submissionImg")) {
            openedSids.push(sid);
            const submissionImg = submissionPage.getElementById("submissionImg");
            submissionImg.setAttribute('imgno', openedSids.length - 1);
            submissionImg.addEventListener('click', submissionImgOnClick);

            insertAfter(submissionImg, lastSubmissionImg);
            insertBreakBefore(submissionImg);
            insertBreakBefore(submissionImg);

            return submissionPage;
        }
    }
}

async function loadPageBefore(sid, lastSubmissionImg) {
    if (sid && !openedSids.includes(sid)) {
        const submissionPage = await requestHelper.SubmissionRequests.getSubmissionPage(sid);
        if (submissionPage && submissionPage.getElementById("submissionImg")) {
            openedSids.push(sid);
            const submissionImg = submissionPage.getElementById("submissionImg");
            submissionImg.setAttribute('imgno', openedSids.length - 1);
            submissionImg.addEventListener('click', submissionImgOnClick);

            insertBefore(submissionImg, lastSubmissionImg);
            insertBreakBefore(submissionImg);
            insertBreakBefore(submissionImg);

            return submissionPage;
        }
    }
}

async function searchAllSimularPages() {
    const submissionPage = document.getElementById("submission_page");
    const container = submissionPage.querySelector('div[class="submission-id-sub-container"]');
    let currTitle = container.querySelector('div[class="submission-title"]').querySelector('p').textContent;
    const isFirst = currTitle.includes("1");
    currTitle = generalizeString(currTitle, true, true, true, true, true);
    const author = container.querySelector('a[href]');

    let user = author.href;
    if (user.endsWith("/"))
        user = user.substring(0, user.length - 1);
    user = user.substring(user.lastIndexOf("/") + 1);

    const sid = getIdFromUrl(window.location.toString());

    const galleryPages = await requestHelper.UserRequests.GalleryRequests.Gallery.getFiguresTillId(user, sid);
    const simularFigures = [];
    let currPage = 1;
    for (const figures of galleryPages) {
        for (const figure of figures) {
            const title = getTitleFromFigureGeneralized(figure);
            if (title != "" && (title.includes(currTitle) || currTitle.includes(title))) {
                if (figure.id.toString().replace('sid-', '') != sid) {
                    simularFigures.push(figure);
                }
            }
        }
        currPage++;
    }

    const simularFiguresBefore = [];
    if (isFirst === false && backwardSearchSetting.value !== 0) {
        const galleryPagesBefore = await requestHelper.UserRequests.GalleryRequests.Gallery.getFiguresSinceIdTillPage(user, sid, currPage + backwardSearchSetting.value);
        if (galleryPagesBefore) {
            for (const figures of galleryPagesBefore) {
                for (const figure of figures) {
                    const title = getTitleFromFigureGeneralized(figure);
                    if (title != "" && (title.includes(currTitle) || currTitle.includes(title))) {
                        if (figure.id.toString().replace('sid-', '') != sid) {
                            simularFiguresBefore.push(figure);
                        }
                    }
                }
            }
        }
    }

    if (simularFigures.length === 0 && simularFiguresBefore.length === 0)
        return false;

    simularFigures.reverse();
    const simularSids = simularFigures.map(figure => figure.id.toString().replace('sid-', ''));

    simularFiguresBefore.reverse();
    const simularSidsBefore = simularFiguresBefore.map(figure => figure.id.toString().replace('sid-', ''));

    openedSids = [];
    let lastSubmissionImg = document.getElementById("submissionImg");
    if (simularSidsBefore.length !== 0) {
        rootSubmissionImg.setAttribute('imgno', -1);
        await loadPageBefore(simularSidsBefore[0], lastSubmissionImg);
        for (const sid of simularSidsBefore) {
            await loadPage(sid, lastSubmissionImg);
            lastSubmissionImg = [...document.querySelectorAll('img[imgno="' + (openedSids.length - 1) + '"]')].pop();
        }
        lastSubmissionImg = document.querySelector('img[rootSubmissionImg="true"]');
        lastSubmissionImg.setAttribute('imgno', openedSids.length);
        insertBreakBefore(lastSubmissionImg);
        insertBreakBefore(lastSubmissionImg);
    }
    openedSids.push(getIdFromUrl(window.location.toString()));

    for (const sid of simularSids) {
        await loadPage(sid, lastSubmissionImg);
        lastSubmissionImg = [...document.querySelectorAll('img[imgno="' + (openedSids.length - 1) + '"]')].pop();
    }
    return true;
}

function getTitleFromFigure(figure) {
    const figcaption = figure.querySelector('figcaption');
    let title = figcaption.querySelector('a[href]').textContent;
    return title;
}

function getTitleFromFigureGeneralized(figure) {
    const figcaption = figure.querySelector('figcaption');
    let title = figcaption.querySelector('a[href]').textContent;
    title = generalizeString(title, true, true, true, true, true);
    return title;
}

function getIdFromUrl(url) {
    try {
        const firstNumberIndex = url.search(/\d/);
        const lastNumberIndex = url.lastIndexOf(url.match(/\d(?=\D*$)/));
        const id = url.substring(firstNumberIndex, lastNumberIndex + 1);
        return id;
    } catch {
        return;
    }
}

function submissionImgOnClick(event) {
    const img = event.target;
    if (document.querySelectorAll('img[imgno]').length > 1) {
        showLightBox(img);
    }
    event.preventDefault();
}

function showLightBox(img) {
    const lightbox = document.createElement('div');
    lightbox.className = 'lightbox lightbox-submission';
    lightbox.onclick = () => {
        document.body.removeChild(lightbox);
        lightboxPresent = false;
        currLightboxNo = -1;
        window.removeEventListener('keydown', handleArrowKeys);
    };
    const lightboxImg = img.cloneNode(false);
    lightbox.appendChild(lightboxImg);
    document.body.appendChild(lightbox);
    lightboxPresent = true;
    currLightboxNo = +img.getAttribute('imgno');
    window.addEventListener('keydown', handleArrowKeys);
}

function navigateLightboxLeft() {
    if (currLightboxNo > 0) {
        currLightboxNo--;
        const lightbox = document.body.querySelector('div[class="lightbox lightbox-submission"]');
        const lightboxImg = lightbox.querySelector('img');
        const nextImg = document.querySelector('img[imgno="' + currLightboxNo + '"]');
        lightboxImg.src = nextImg.src;
    }
}

function navigateLightboxRight() {
    if (currLightboxNo < openedSids.length - 1) {
        currLightboxNo++;
        const lightbox = document.body.querySelector('div[class="lightbox lightbox-submission"]');
        const lightboxImg = lightbox.querySelector('img');
        const nextImg = document.querySelector('img[imgno="' + currLightboxNo + '"]');
        lightboxImg.src = nextImg.src;
    }
}

function handleArrowKeys(event) {
    if (event.keyCode === 37) { // left arrow
        navigateLightboxLeft();
    } else if (event.keyCode === 38) { // up arrow
        navigateLightboxLeft();
    } else if (event.keyCode === 39) { // right arrow
        navigateLightboxRight();
    } else if (event.keyCode === 40) { // down arrow
        navigateLightboxRight();
    }
    event.preventDefault();
}

function insertAfter(newNode, referenceNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}

function insertBefore(newNode, referenceNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode);
}

function insertBreakAfter(referenceNode) {
    insertAfter(document.createElement("br"), referenceNode);
}

function insertBreakBefore(referenceNode) {
    referenceNode.parentNode.insertBefore(document.createElement("br"), referenceNode);
}

function generalizeString(inputString, textToNumbers, removeSpecialChars, removeNumbers, removeSpaces, removeRoman) {
    let outputString = inputString.toLowerCase();

    if (removeRoman) {
        const roman = ["i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x", "xi", "xii", "xiii", "xiv", "xv", "xvi", "xvii", "xviii", "xix", "xx"]; //Checks only up to 20
        outputString = outputString.replace(new RegExp(`(?:^|[^a-zA-Z])(${roman.join("|")})(?:[^a-zA-Z]|$)`, "g"), "");
    }

    if (textToNumbers) {
        const numbers = { zero: 0, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10, eleven: 11, twelve: 12, thirteen: 13, fourteen: 14, fifteen: 15, sixteen: 16, seventeen: 17, eighteen: 18, nineteen: 19, twenty: 20, thirty: 30, forty: 40, fifty: 50, sixty: 60, seventy: 70, eighty: 80, ninety: 90, hundred: 100 };
        outputString = outputString.replace(new RegExp(Object.keys(numbers).join("|"), "gi"), match => numbers[match.toLowerCase()]);
    }

    if (removeSpecialChars)
        outputString = outputString.replace(/[^a-zA-Z0-9 ]/g, "");

    if (removeNumbers)
        outputString = outputString.replace(/[^a-zA-Z ]/g, "");

    if (removeSpaces)
        outputString = outputString.replace(/\s/g, "");

    return outputString;
}