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