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

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

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

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

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

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