FA Fast Favoriter

Gives you a Fav Button in any Gallery so you can add an image to your favorites without clicking on it.

// ==UserScript==
// @name        FA Fast Favoriter
// @namespace   Violentmonkey Scripts
// @match       *://*.furaffinity.net/*
// @require 	https://update.greasyfork.org/scripts/475041/1267274/Furaffinity-Custom-Settings.js
// @grant       none
// @version     3.2.3
// @author      Midori Dragon
// @description Gives you a Fav Button in any Gallery so you can add an image to your favorites without clicking on it.
// @icon        https://www.furaffinity.net/themes/beta/img/banners/fa_logo.png?v2
// @homepageURL https://greasyfork.org/de/scripts/452743-fast-favoriter-2
// @supportURL  https://greasyfork.org/de/scripts/452743-fast-favoriter-2/feedback
// @license     MIT
// ==/UserScript==

// jshint esversion: 8

const matchList = ['net/browse', 'net/gallery', 'net/search', 'net/favorites', 'net/controls/favorites', 'net/controls/submissions', 'net/msg/submissions' ];

CustomSettings.name = "Extension Settings";
CustomSettings.provider = "Midori's Script Settings";
CustomSettings.headerName = `${GM_info.script.name} Settings`;
const waitForWFLoadingSetting = CustomSettings.newSetting("Wait for WF", "Sets wether to wait for WF-Loading to finish before starting to load the Fav buttons. Unnecessary if Watches Favorite Viewer isn't installed.", SettingTypes.Boolean, "Wait for WF-Loading", true);
const loadingSpinSpeedSetting = CustomSettings.newSetting("Fav Loading Animation", "Sets the spinning speed of the loading animation in milliseconds.", SettingTypes.Number, "", 100);
CustomSettings.loadSettings();

if (window.parent !== window) {
  document.addEventListener('DOMContentLoaded', function() {
    let srcs = document.querySelectorAll('[src]');
    for (let src of srcs)
      src.removeAttribute('src');
  });
  let srcs = document.querySelectorAll('[src]');
  for (let src of srcs)
    src.removeAttribute('src');
  return;
}

let color = "color: blue";
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
    color = "color: aqua";

if (window.location.toString().includes("?extension")) {
	console.info(`%cSettings: ${GM_info.script.name} v${GM_info.script.version}`, color);
	return;
}

if (!matchList.some(x => window.location.toString().includes(x)))
  return;

console.info(`%cRunning: ${GM_info.script.name} v${GM_info.script.version} ${CustomSettings.toString()}`, color);

let rcount = 0;
let running = false;
let queue = [];

window.addEventListener("load", () => {
  start();
});

async function start() {
  queue.push(createButtons);
  waitForRun();
}

function doWaitForWFLoading() {
  return new Promise((resolve) => {
    if (waitForWFLoadingSetting.value) {
      let canContinue = document.getElementById("wfButton") == null;
      const intervalId = setInterval(() => {
        if (canContinue) {
          clearInterval(intervalId);
          resolve();
        } else {
          const wfButton = document.getElementById("wfButton");
          if (!wfButton)
            canContinue = true;
          else
            canContinue = wfButton.getAttribute("loading") == false.toString();
        }
      }, 500);
    } else
      resolve();
  });
}


async function waitForRun() {
  await waitUntilFalse(running);
  const next = queue.shift();
  if (next) {
    running = true;
    await next();
    running = false;
    waitForRun();
  }
}

window.updateFastFavoriter = start;

async function createButtons() {
  let promises = [];
  let semaphore = new Semaphore(2);
  let figures = document.querySelectorAll('figure:not([fastfav])');
  for (let i = 0; i < figures.length; i++) {
    figures[i].setAttribute('fastfav', true);
    let imageID = figures[i].id;
    imageID = imageID.substring(imageID.indexOf("-") + 1);
    promises.push(
      semaphore.acquire().then(async () => {
        try {
          await doWaitForWFLoading();
          let favdoc = await getHTML("https://www.furaffinity.net/view/" + imageID);
          let favlink = await getFavLink(favdoc);
          createFavButton(favlink, figures[i], i);
        } finally {
          semaphore.release();
        }
      })
    );
  }
  return Promise.all(promises);
}

async function createButton(figure, i, imageID) {
  rcount++;
  let favdoc = await getHTML("https://www.furaffinity.net/view/" + imageID);
  rcount--;
  let favlink = await getFavLink(favdoc);
  createFavButton(favlink, figure, i);
}

class Semaphore {
  constructor(maxConcurrency) {
    this.maxConcurrency = maxConcurrency;
    this.currentConcurrency = 0;
    this.waitingQueue = [];
  }

  acquire() {
    return new Promise((resolve, reject) => {
      if (this.currentConcurrency < this.maxConcurrency) {
        this.currentConcurrency++;
        resolve();
      } else {
        this.waitingQueue.push(resolve);
      }
    });
  }

  release() {
    if (this.waitingQueue.length > 0) {
      let nextResolve = this.waitingQueue.shift();
      nextResolve();
    } else {
      this.currentConcurrency--;
    }
  }
}

async function favImage(figure, favLink, i, rotation) {
  if (!figure)
    return;

  let footer = document.getElementById("footer");
  let iframe = document.createElement("iframe");
  iframe.id = "favIFrame_" + i;
  iframe.src = favLink;
  iframe.style.display = "none";
  iframe.sandbox = "allow-same-origin";
  iframe.addEventListener("load", async function() {
    let favdoc = iframe.contentDocument;
    footer.removeChild(iframe);
    favLink = await getFavLink(favdoc);
    if (!favLink) {
      checkFavLinkMissingReason(figure, favdoc, rotation);
      return;
    }
    changeFavButtonLink(favLink, figure, i, rotation);
  });
  footer.appendChild(iframe);
}

async function checkFavLinkMissingReason(figure, favdoc, rotation) {
  favOnError(figure, rotation);
  let blocked = favdoc.getElementById("standardpage").querySelector('div[class="redirect-message"]');
  if (blocked && blocked.textContent.includes("blocked"))
    alert(blocked.textContent);
}

async function favOnError(figure, rotation) {
  rotation();
  //Embedded Image Viewer integration <start>
  let embeddedFavButton = document.getElementById("embeddedFavButton");
  if (embeddedFavButton)
    embeddedFavButton.textContent = "x";
  //Embedded Image Viewer integration <end>

  let favButton = figure.querySelector('[type="button"][class="button standard mobile-fix"]');
  if (favButton)
    favButton.textContent = "x";
}

async function createFavButton(favLink, figure, i) {
  let favButton = document.createElement("button");
  favButton.id = "favbutton_" + i;
  favButton.type = "button";
  favButton.className = "button standard mobile-fix";
  if (favLink.includes("unfav"))
    favButton.textContent = "-Fav";
  else
    favButton.textContent = "+Fav";
  favButton.setAttribute("favlink", favLink);
  favButton.style.marginTop = figure.childNodes[1].offsetHeight + 5 + "px";
  favButton.onclick = function() {
    let rotation = rotateText(favButton);
    favImage(figure, favLink, i, rotation);
  };
  figure.style.paddingBottom = figure.childNodes[1].offsetHeight + 36 + 10 + "px";
  insertAfter(favButton, figure.childNodes[figure.childNodes.length - 1]);
}

async function changeFavButtonLink(favLink, figure, i, rotation) {
  let favButton = document.getElementById("favbutton_" + i);
  rotation();
  if (favLink.includes("unfav"))
    favButton.textContent = "-Fav";
  else
    favButton.textContent = "+Fav";
  favButton.setAttribute("favlink", favLink);
  favButton.onclick = function() {
    rotation = rotateText(favButton);
    favImage(figure, favLink, i, rotation);
  };

  //Embedded Image Viewer integration <start>
  let embeddedFavButton = document.getElementById("embeddedFavButton");
  if (embeddedFavButton) {
    if (favLink.includes("unfav"))
      embeddedFavButton.textContent = "-Fav";
    else
      embeddedFavButton.textContent = "+Fav";
    embeddedFavButton.onclick = function() {
      embeddedFavButton.textContent = "...";
      favImage(figure, favLink, i);
    };
  }
  //Embedded Image Viewer integration <end>
}

function rotateText(element) {
  let isRotating = true;
  const characters = [ "◜", "◠", "◝", "◞", "◡", "◟" ];
  let index = 0;

  function update() {
    if (!isRotating) return;
    element.textContent = characters[index % characters.length];
    index++;
    setTimeout(update, loadingSpinSpeedSetting.value);
  }
  if (!isRotating) return;
  update();

  return function stopRotation() {
    isRotating = false;
  };
}

async function insertAfter(newElement, referenceElement) {
  referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling);
}

function waitUntilFalse(bool) {
  return new Promise(resolve => {
    const checkBool = () => {
      if (!bool) {
        resolve();
      } else {
        setTimeout(checkBool, 100);
      }
    };
    checkBool();
  });
}

async function getFavLink(subdoc) {
  let buttons = subdoc.querySelectorAll('a[class="button standard mobile-fix"]');
  for (const button of buttons)
    if (button.textContent.includes("Fav") && button.textContent.length <= 4)
      return button.href;
}

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);
  }
}