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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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