// ==UserScript==
// @name Webcomic Autoloader 2.0
// @namespace Violentmonkey Scripts
// @match *://*.furaffinity.net/*
// @grant none
// @version 1.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
//User Options:
let showSearchButton = JSON.parse(localStorage.getItem("wasetting_1"));
if (showSearchButton == null || showSearchButton == undefined)
showSearchButton = true;
let loadingSpinSpeed = +localStorage.getItem("wasetting_2");
if (loadingSpinSpeed == null || loadingSpinSpeed == undefined || loadingSpinSpeed == 0)
loadingSpinSpeed = 100;
const matchList = ['net/view' ];
let settingsCount = 0;
const isSettings = window.location.toString().includes('controls/settings');
let exSettings = JSON.parse(localStorage.getItem("wasettings"));
if (exSettings == null)
exSettings = false;
addExSettings();
if (isSettings) {
addExSettingsSidebar();
if (exSettings)
createSettings();
}
if (window.parent !== window)
return;
if (!matchList.some(x => window.location.toString().includes(x)))
return;
console.info('%cRunning: Webcomic Autoloader', 'color: blue');
let lightboxPresent = false;
let currLighboxNo = -1;
let imgCount = 1;
let rootHolder = document.getElementById("submissionImg");
rootHolder.setAttribute('imgno', 0);
rootHolder.onclick = function() {
lightboxPresent = true;
currLighboxNo = 0;
window.addEventListener('keydown', handleArrowKeys);
let lightbox = document.body.querySelector('div[class="lightbox lightbox-submission"]');
lightbox.addEventListener('click', function() {
lightboxPresent = false;
currLighboxNo = -1;
window.removeEventListener('keydown', handleArrowKeys);
});
};
let counter = 5;
let showLinks = false;
let startImg = window.location.href;
while (startImg.endsWith("/"))
startImg = startImg.substring(0, startImg.length - 1);
let pageCounter = 0;
let openedLinks = [document.location.toString()];
let rotation;
function insertAfter(newElement, referenceElement) {
referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling);
}
function insertBreakAfter(referenceElement) {
let br = document.createElement("br");
insertAfter(br, referenceElement);
}
function getNextLink(doc) {
let links = doc.querySelectorAll('a[href]:not([class*="button standard mobile-fix"]), :not([class])');
let link;
for (const elem of links) {
if (elem.href && elem.textContent.toLowerCase().includes("next")) {
try {
let currImgCalc = elem.href;
while (currImgCalc.endsWith("/"))
currImgCalc = currImgCalc.substring(0, currImgCalc.length - 1);
currImgCalc = currImgCalc.substring(currImgCalc.lastIndexOf('/'), currImgCalc.length);
let startImgCalc = startImg.substring(startImg.lastIndexOf('/'), startImg.length);
if (currImgCalc != startImgCalc && currImgCalc != "/#" && !openedLinks.includes(elem.href)) {
link = elem.href;
openedLinks.push(link);
pageCounter++;
} else if (openedLinks.includes(elem.href)) {
return "loopinglink";
}
} catch (ex) { console.error(ex); }
}
}
return link;
}
function loadNextPage(nextLink) {
if(nextLink) {
if (nextLink.includes("http:"))
nextLink = nextLink.replace(/^http:/, "https:");
let request = new XMLHttpRequest();
request.open('GET', nextLink, true);
request.onload = function() {
if (this.status >= 200 && this.status < 400) {
// Success!
parser = new DOMParser();
let nextPage = parser.parseFromString(this.response, "text/html");
let nl;
if (nextPage && nextPage.getElementById("submissionImg")) {
nl = getNextLink(nextPage);
imgCount++;
let img = nextPage.getElementById("submissionImg");
img.setAttribute('imgno', imgCount - 1);
img.onclick = function() {
doLightBox(img);
};
rootHolder.parentNode.insertBefore(img, rootHolder.nextSibling);
rootHolder = rootHolder.nextSibling;
insertBreakAfter(rootHolder);
rootHolder = rootHolder.nextSibling;
if (showLinks) {
let lnk = document.createElement('a');
let lnkURL = nextLink;
lnk.innerHTML = lnkURL;
lnk.href = lnkURL;
insertAfter(lnk, rootHolder);
rootHolder = rootHolder.nextSibling;
insertBreakAfter(rootHolder);
rootHolder = rootHolder.nextSibling;
}
else {
let br = document.createElement('br');
insertAfter(br, rootHolder);
rootHolder = rootHolder.nextSibling;
}
}
if (nl == "loopinglink") {
// searchNextSimularPage();
return;
} else if (nl)
loadNextPage(nl);
} else {
//We reached our target server, but it returned an error
console.log("none");
}
};
request.onerror = function() {
//There was a connection error of some sort
console.log("error");
};
request.send();
}
}
function loadPages(links, i) {
if (i == links.length)
return;
let nextLink = links[i];
if(nextLink) {
let request = new XMLHttpRequest();
request.open('GET', nextLink, true);
request.onload = function() {
if (this.status >= 200 && this.status < 400) {
// Success!
parser = new DOMParser();
let nextPage = parser.parseFromString(this.response, "text/html");
if (nextPage && nextPage.getElementById("submissionImg")) {
imgCount++;
let img = nextPage.getElementById("submissionImg");
img.setAttribute('imgno', imgCount - 1);
img.onclick = function() {
doLightBox(img);
};
rootHolder.parentNode.insertBefore(img, rootHolder.nextSibling);
rootHolder = rootHolder.nextSibling;
insertBreakAfter(rootHolder);
rootHolder = rootHolder.nextSibling;
if (showLinks) {
let lnk = document.createElement('a');
let lnkURL = nextLink;
lnk.innerHTML = lnkURL;
lnk.href = lnkURL;
insertAfter(lnk, rootHolder);
rootHolder = rootHolder.nextSibling;
insertBreakAfter(rootHolder);
rootHolder = rootHolder.nextSibling;
}
else {
let br = document.createElement('br');
insertAfter(br, rootHolder);
rootHolder = rootHolder.nextSibling;
}
}
i++;
loadPages(links, i);
} else {
//We reached our target server, but it returned an error
console.log("none");
}
};
request.onerror = function() {
//There was a connection error of some sort
console.log("error");
};
request.send();
}
}
function doLightBox(img) {
let lightbox = document.createElement('div');
lightbox.className = 'lightbox lightbox-submission';
lightbox.onclick = function() {
document.body.removeChild(lightbox);
lightboxPresent = false;
currLighboxNo = -1;
window.removeEventListener('keydown', handleArrowKeys);
};
let lightboxImg = img.cloneNode(false);
lightboxImg.onclick = function(){};
lightbox.appendChild(lightboxImg);
document.body.appendChild(lightbox);
lightboxPresent = true;
currLighboxNo = +img.getAttribute('imgno');
window.addEventListener('keydown', handleArrowKeys);
}
function navigateLightboxLeft() {
if (currLighboxNo > 0) {
currLighboxNo--;
let lightbox = document.body.querySelector('div[class="lightbox lightbox-submission"]');
let lightboxImg = lightbox.querySelector('img');
let nextImg = document.querySelector('img[imgno="' + currLighboxNo + '"]');
lightboxImg.src = nextImg.src;
}
}
function navigateLightboxRight() {
if (currLighboxNo < imgCount - 1) {
currLighboxNo++;
let lightbox = document.body.querySelector('div[class="lightbox lightbox-submission"]');
let lightboxImg = lightbox.querySelector('img');
let nextImg = document.querySelector('img[imgno="' + currLighboxNo + '"]');
lightboxImg.src = nextImg.src;
}
}
function handleArrowKeys(event) {
if (event.keyCode === 37) { // left arrow
navigateLightboxLeft();
} else if (event.keyCode === 39) { // right arrow
navigateLightboxRight();
}
}
function startAutoloader() {
let ab = document.getElementById("autoloaderButton");
ab.parentNode.removeChild(ab);
let checkbox = document.getElementById("linksCheckbox");
checkbox.parentNode.removeChild(checkbox);
let label = document.getElementById("showLinksLabel");
label.parentNode.removeChild(label);
insertBreakAfter(rootHolder);
rootHolder = rootHolder.nextSibling;
loadNextPage(secondPage);
}
async function searchNextSimularPage() {
let ab = document.getElementById("morePagesSearch");
if (ab)
rotation = rotateText(ab, true);
let checkbox = document.getElementById("linksCheckbox");
if (checkbox)
checkbox.parentNode.removeChild(checkbox);
let label = document.getElementById("showLinksLabel");
if (label)
label.parentNode.removeChild(label);
if (checkbox) {
insertBreakAfter(rootHolder);
rootHolder = rootHolder.nextSibling;
}
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;
currTitle = generalizeString(currTitle, true, true, true, true, true);
const author = container.querySelector('a');
let galleryLink = author.href.substring(0, author.href.length - 1);
let currentSubmissionId = window.location.toString().substring(0, window.location.toString().length - 1);
currentSubmissionId = "sid-" + currentSubmissionId.substring(currentSubmissionId.lastIndexOf("/") + 1);
galleryLink = galleryLink.substring(galleryLink.lastIndexOf("/") + 1);
galleryLink = "https://www.furaffinity.net/gallery/" + galleryLink;
let figures = [];
let currentFigureIndex = -1;
let j = 0;
while (currentFigureIndex == -1) {
j++;
gallery = await getHTML(galleryLink + "/" + j);
let figuresNew = gallery.getElementsByTagName("figure");
if (!figuresNew || figuresNew.length == 0) {
rotation.stop();
ab.value = "Nothing found... Search again";
return false;
}
figuresNew = Array.from(figuresNew);
figures = figures.concat(figuresNew);
currentFigureIndex = figuresNew.findIndex(figure => figure.id == currentSubmissionId);
//console.log("j: " + j + " | index: " + currentFigureIndex);
}
currentFigureIndex = figures.findIndex(figure => figure.id == currentSubmissionId);
//console.log("total index: " + currentFigureIndex);
if (currentFigureIndex == 0) {
rotation.stop();
ab.value = "Nothing found... Search again";
return false;
}
figures = figures.slice(0, currentFigureIndex);
let pages = [];
for (let i = figures.length - 1; i >= 0; i--) {
const titleElem = figures[i].querySelector('figcaption').querySelector('a');
const title = generalizeString(titleElem.getAttribute("title"), true, true, true, true, true);
if (title.includes(currTitle))
pages.push(titleElem.href);
}
if (pages.length == 0) {
rotation.stop();
ab.value = "Nothing found... Search again";
return false;
}
else
loadPages(pages, 0);
ab.parentNode.removeChild(ab);
return true;
}
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;
}
function rotateText(element, isRotating) {
const characters = [ "◜", "◠", "◝", "◞", "◡", "◟" ];
let index = 0;
let intervalId;
function updateText() {
element.value = characters[index];
index = (index + 1) % characters.length;
}
function startRotation() {
intervalId = setInterval(updateText, loadingSpinSpeed);
}
function stopRotation() {
clearInterval(intervalId);
}
if (isRotating) {
startRotation();
}
return {
start: startRotation,
stop: stopRotation
}
}
function setShowLinks() {
if (showLinks)
showLinks = false;
else
showLinks = true;
let checkbox = document.getElementById("linksCheckbox");
checkbox.checked = showLinks;
}
async function getHTML(url) {
try {
const response = await fetch(url);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc;
} catch (error) {
console.error(error);
}
}
let secondPage = getNextLink(document);
if(secondPage) {
let img = document.getElementById("submissionImg");
insertBreakAfter(rootHolder);
rootHolder = rootHolder.nextSibling;
pageCounter++;
let label = document.createElement('a');
label.id = "showLinksLabel";
label.innerHTML = "Show Links";
label.style.cursor = "pointer";
label.style.marginBottom = "30px";
label.style.marginLeft = "5px";
label.onclick = setShowLinks;
insertAfter(label, rootHolder);
let checkbox = document.createElement('input');
checkbox.value = "Show Links";
checkbox.type = "checkbox";
checkbox.id = "linksCheckbox";
checkbox.className = "checkbox standard mobile-fix";
checkbox.style.cursor = "pointer";
checkbox.style.marginBottom = "30px";
checkbox.style.marginLeft = "20px";
checkbox.onclick = setShowLinks;
insertAfter(checkbox, rootHolder);
let button = document.createElement('input');
button.value = "Enable Comic Autoloader";
button.type = "button";
button.id = "autoloaderButton";
button.className = "button standard mobile-fix";
button.style.marginTop = "10px";
button.style.marginBottom = "20px";
button.onclick = startAutoloader;
insertAfter(button, rootHolder);
} else if (showSearchButton) {
let img = document.getElementById("submissionImg");
insertBreakAfter(rootHolder);
rootHolder = rootHolder.nextSibling;
let label = document.createElement('a');
label.id = "showLinksLabel";
label.innerHTML = "Show Links";
label.style.cursor = "pointer";
label.style.marginBottom = "30px";
label.style.marginLeft = "5px";
label.onclick = setShowLinks;
insertAfter(label, rootHolder);
let checkbox = document.createElement('input');
checkbox.value = "Show Links";
checkbox.type = "checkbox";
checkbox.id = "linksCheckbox";
checkbox.className = "checkbox standard mobile-fix";
checkbox.style.cursor = "pointer";
checkbox.style.marginBottom = "30px";
checkbox.style.marginLeft = "20px";
checkbox.onclick = setShowLinks;
insertAfter(checkbox, rootHolder);
let button = document.createElement('input');
button.value = "Search for more Pages in Series";
button.type = "button";
button.id = "morePagesSearch";
button.className = "button standard mobile-fix";
button.style.marginTop = "10px";
button.style.marginBottom = "20px";
button.onclick = searchNextSimularPage;
insertAfter(button, rootHolder);
}
// ------------------------------ //
// ---------- SETTINGS ---------- //
// ------------------------------ //
async function addExSettings() {
const settings = document.querySelector('ul[class="navhideonmobile"]').querySelector('a[href="/controls/settings/"]').parentNode;
if (document.getElementById("extension_settings")) {
document.getElementById('midori_settings').addEventListener('click', function() { localStorage.setItem("wasettings", true.toString()); });
return;
}
let exSettingsHeader = document.createElement("h3");
exSettingsHeader.id = "extension_settings";
exSettingsHeader.textContent = "Extension Settings";
settings.appendChild(exSettingsHeader);
let wasettings = document.createElement("a");
wasettings.id = "midori_settings";
wasettings.textContent = "Midori's Script Settings";
wasettings.style.cursor = "pointer";
wasettings.onclick = function() {
localStorage.setItem("wasettings", true.toString());
window.location = "https://www.furaffinity.net/controls/settings";
}
settings.appendChild(wasettings);
}
async function addExSettingsSidebar() {
const settings = document.getElementById('controlpanelnav');
if (document.getElementById("extension_settings_side")) {
document.getElementById('midori_settings_side').addEventListener('click', function() { localStorage.setItem("wasettings", true.toString()); });
return;
}
let exSettingsHeader = document.createElement("h3");
exSettingsHeader.id = "extension_settings_side";
exSettingsHeader.textContent = "Extension Settings";
settings.appendChild(exSettingsHeader);
let wasettings = document.createElement("a");
wasettings.id = "midori_settings_side";
wasettings.textContent = "Midori's Script Settings";
wasettings.style.cursor = "pointer";
wasettings.onclick = function() {
localStorage.setItem("wasettings", true.toString());
window.location = "https://www.furaffinity.net/controls/settings";
}
settings.appendChild(wasettings);
}
async function createSettings() {
localStorage.setItem("wasettings", false.toString());
const columnPage = document.getElementById("columnpage");
const content = columnPage.querySelector('div[class="content"]');
for (const section of content.querySelectorAll('section:not([class="exsettings"])'))
section.parentNode.removeChild(section);
const section = document.createElement("section");
section.className = 'exsettings';
const headerContainer = document.createElement("div");
headerContainer.className = "section-header";
const header = document.createElement("h2");
header.textContent = "Webcomic Autoloader Settings";
headerContainer.appendChild(header);
section.appendChild(headerContainer);
const bodyContainer = document.createElement("div");
bodyContainer.className = "section-body";
// Simular Search Button Settings
const simularSearchButtonSetting = createSetting("Simular Search Button", "Sets wether the search for simular Pages button is shown", "boolean", "Show Search Button", (target) => {
showSearchButton = target.checked;
localStorage.setItem(target.id, showSearchButton.toString());
});
simularSearchButtonSetting.querySelector('[id*="setting"]').checked = showSearchButton;
bodyContainer.appendChild(simularSearchButtonSetting);
// Loading Animation Setting
const loadingAnimationSetting = createSetting("Loading Animation", "Sets the spinning speed of the loading animation in milliseconds", "number", "", (target) => {
loadingSpinSpeed = +target.value;
localStorage.setItem(target.id, loadingSpinSpeed.toString());
});
loadingAnimationSetting.querySelector('[id*="setting"]').value = loadingSpinSpeed;
bodyContainer.appendChild(loadingAnimationSetting);
section.appendChild(bodyContainer);
content.appendChild(section);
}
function createSetting(name, description, type, typeDescription, executeFunction) {
const settingContainer = document.createElement("div");
settingContainer.className = "control-panel-item-container";
const settingName = document.createElement("div");
settingName.className = "control-panel-item-name";
const settingNameText = document.createElement("h4");
settingNameText.textContent = name;
settingName.appendChild(settingNameText);
settingContainer.appendChild(settingName);
const settingDesc = document.createElement("div");
settingDesc.className = "control-panel-item-description";
const settingDescText = document.createTextNode(description);
settingDesc.appendChild(settingDescText);
settingContainer.appendChild(settingDesc);
const settingOption = document.createElement("div");
settingOption.className = "control-panel-item-options";
if (type === "number") {
settingsCount++;
const settingInput = document.createElement("input");
settingInput.id = "igsetting_" + settingsCount;
settingInput.type = "text";
settingInput.className = "textbox";
settingInput.addEventListener("keydown", (event) => {
const currentValue = parseInt(settingInput.value) || 0;
if (event.key === "ArrowUp") {
settingInput.value = (currentValue + 1).toString();
executeFunction(settingInput);
} else if (event.key === "ArrowDown") {
if (currentValue != 0)
settingInput.value = (currentValue - 1).toString();
executeFunction(settingInput);
}
});
settingInput.addEventListener("input", () => {
settingInput.value = settingInput.value.replace(/[^0-9]/g, "");
if (settingInput.value < 0)
settingInput.value = 0;
});
settingInput.addEventListener("input", () => executeFunction(settingInput));
settingOption.appendChild(settingInput);
} else if (type === "boolean") {
settingsCount++;
const settingCheckbox = document.createElement("input");
settingCheckbox.id = "wfsetting_" + settingsCount;
settingCheckbox.type = "checkbox";
settingCheckbox.style.cursor = "pointer";
settingCheckbox.style.marginRight = "4px";
settingCheckbox.addEventListener("change", () => executeFunction(settingCheckbox));
settingOption.appendChild(settingCheckbox);
const settingOptionLabel = document.createElement("label");
settingOptionLabel.textContent = typeDescription;
settingOptionLabel.style.cursor = "pointer";
settingOptionLabel.addEventListener("click", () => {
settingCheckbox.checked = !settingCheckbox.checked;
executeFunction(settingCheckbox);
});
settingOption.appendChild(settingOptionLabel);
} else if (type === "action") {
settingsCount++;
const settingButton = document.createElement("button");
settingButton.id = "wfsetting_" + settingsCount;
settingButton.type = "button";
settingButton.className = "button standard mobile-fix";
settingButton.textContent = typeDescription;
settingButton.addEventListener("click", () => executeFunction(settingButton));
settingOption.appendChild(settingButton);
}
settingContainer.appendChild(settingOption);
return settingContainer;
}