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

当前为 2025-01-12 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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.7
// @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())];

function CheckTags(element) {
		var _a;
		if (!("1" === document.body.getAttribute("data-user-logged-in"))) return;
		const tagsHideMissingTags = "1" === document.body.getAttribute("data-tag-blocklist-hide-tagless"), tags = null === (_a = element.getAttribute("data-tags")) || void 0 === _a ? void 0 : _a.trim().split(/\s+/);
		let blockReason = "";
		if (null != tags && tags.length > 0 && "" !== tags[0]) {
				const blockedTags = function getBannedTags(tags) {
						var _a;
						const tagsBlocklist = null !== (_a = document.body.getAttribute("data-tag-blocklist")) && void 0 !== _a ? _a : [];
						let bTags = [];
						if (null == tags || 0 === tags.length) return [];
						for (const tag of tags) for (const blockedTag of tagsBlocklist) tag === blockedTag && bTags.push(blockedTag);
						return [ ...new Set(bTags) ];
				}(tags);
				if (blockedTags.length <= 0) setBlockedState(element, !1); else {
						setBlockedState(element, !0), blockReason = "Blocked tags:\n";
						for (const tag of blockedTags) blockReason += "• " + tag + "\n";
				}
		} else setBlockedState(element, tagsHideMissingTags), tagsHideMissingTags && (blockReason = "Content is missing tags.");
		"" !== blockReason && "submissionImg" !== element.id && element.setAttribute("title", blockReason);
}
function setBlockedState(element, isBlocked) {
		element.classList[isBlocked ? "add" : "remove"]("blocked-content");
}
function CheckTagsAll(doc) {
		if (null == doc) return;
		doc.querySelectorAll("img[data-tags]").forEach((element => CheckTags(element)));
}

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);
    }
		CheckTagsAll(document);
}

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";
		CheckTagsAll(document);
}

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;
}