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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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;
}