Webcomic Autoloader 2.0

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

目前为 2023-05-19 提交的版本。查看 最新版本

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