// ==UserScript==
// @name FA Embedded Image Viewer
// @namespace Violentmonkey Scripts
// @match *://*.furaffinity.net/*
// @require https://updategreasyfork.deno.dev/scripts/475041/1267274/Furaffinity-Custom-Settings.js
// @require https://updategreasyfork.deno.dev/scripts/483952/1486330/Furaffinity-Request-Helper.js
// @require https://updategreasyfork.deno.dev/scripts/485153/1316289/Furaffinity-Loading-Animations.js
// @require https://updategreasyfork.deno.dev/scripts/476762/1318215/Furaffinity-Custom-Pages.js
// @require https://updategreasyfork.deno.dev/scripts/485827/1326313/Furaffinity-Match-List.js
// @require https://updategreasyfork.deno.dev/scripts/492931/1363921/Furaffinity-Submission-Image-Viewer.js
// @grant GM_info
// @version 2.3.0
// @author Midori Dragon
// @description Embeds the clicked Image on the Current Site, so you can view it without loading the submission Page
// @icon https://www.furaffinity.net/themes/beta/img/banners/fa_logo.png?v2
// @license MIT
// ==/UserScript==
// jshint esversion: 8
(() => {
"use strict";
var __webpack_require__ = {
d: (exports, definition) => {
for (var key in definition) __webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key) && Object.defineProperty(exports, key, {
enumerable: !0,
get: definition[key]
});
},
o: (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
};
__webpack_require__.d({}, {
qJ: () => alwaysZoomCenterSetting,
yr: () => closeEmbedAfterOpenSetting,
xe: () => loadingSpinSpeedFavSetting,
_d: () => loadingSpinSpeedSetting,
h_: () => openInNewTabSetting,
e2: () => previewQualitySetting,
uL: () => requestHelper,
t0: () => useCtrlForZoomSetting
});
class EmbeddedCSS {
constructor() {
this.createStyle();
}
createStyle() {
if (document.getElementById("embeddedStyle")) return;
const style = document.createElement("style");
style.id = "embeddedStyle", style.type = "text/css", style.innerHTML = EmbeddedCSS.css,
document.head.appendChild(style);
}
static get css() {
return "\n#embeddedElem {\n position: fixed;\n width: 100vw;\n height: 100vh;\n max-width: 1850px;\n z-index: 999999;\n background: rgba(30,33,38,.65);\n}\n#embeddedBackgroundElem {\n position: fixed;\n display: flex;\n flex-direction: column;\n left: 50%;\n transform: translate(-50%, 0%);\n margin-top: 20px;\n padding: 20px;\n background: rgba(30,33,38,.90);\n border-radius: 10px;\n}\n.embeddedSubmissionImg {\n max-width: inherit;\n max-height: inherit;\n border-radius: 10px;\n user-select: none;\n}\n#embeddedButtonsContainer {\n position: relative;\n margin-top: 20px;\n margin-bottom: 20px;\n margin-left: 20px;\n}\n#embeddedButtonsWrapper {\n display: flex;\n justify-content: center;\n align-items: center;\n}\n#previewLoadingSpinnerContainer {\n position: absolute;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n}\n.embeddedButton {\n margin-left: 4px;\n margin-right: 4px;\n user-select: none;\n}";
}
}
class EmbeddedHTML {
generateHtmlString() {
return EmbeddedHTML.generateHtmlString();
}
static generateHtmlString() {
return '\n<div id="embeddedBackgroundElem">\n <a id="embeddedSubmissionContainer"></a>\n <div id="embeddedButtonsContainer">\n <div id="embeddedButtonsWrapper">\n <a id="embeddedFavButton" type="button" class="embeddedButton button standard mobile-fix">⠀⠀</a>\n <a id="embeddedDownloadButton" type="button" class="embeddedButton button standard mobile-fix">Download</a>\n <a id="embeddedOpenButton" type="button" class="embeddedButton button standard mobile-fix">Open</a>\n <a id="embeddedOpenGalleryButton" type="button" class="embeddedButton button standard mobile-fix" style="display: none;">Open Gallery</a>\n <a id="embeddedCloseButton" type="button" class="embeddedButton button standard mobile-fix">Close</a>\n </div>\n <div id="previewLoadingSpinnerContainer"></div>\n </div>\n</div>';
}
}
class EmbeddedImage {
constructor(figure) {
this._imageLoaded = !1, this.embeddedElem, this.submissionImg, this.favRequestRunning = !1,
this.downloadRequestRunning = !1, this._onRemoveAction, this.createElements(figure);
const submissionContainer = document.getElementById("embeddedSubmissionContainer"), previewLoadingSpinnerContainer = document.getElementById("previewLoadingSpinnerContainer");
this.loadingSpinner = new LoadingSpinner(submissionContainer), this.loadingSpinner.delay = loadingSpinSpeedSetting.value,
this.loadingSpinner.spinnerThickness = 6, this.loadingSpinner.visible = !0, this.previewLoadingSpinner = new LoadingSpinner(previewLoadingSpinnerContainer),
this.previewLoadingSpinner.delay = loadingSpinSpeedSetting.value, this.previewLoadingSpinner.spinnerThickness = 4,
this.previewLoadingSpinner.size = 40, this._onDocumentClick = this._onDocumentClick.bind(this),
document.addEventListener("click", this._onDocumentClick), this.fillSubDocInfos(figure);
}
static get embeddedExists() {
return !!document.getElementById("embeddedElem");
}
onRemove(action) {
this._onRemoveAction = action;
}
remove() {
this.embeddedElem.parentNode.removeChild(this.embeddedElem), document.removeEventListener("click", this._onDocumentClick),
this._onRemoveAction && this._onRemoveAction();
}
_onDocumentClick(event) {
event.target === document.documentElement && this.remove();
}
createElements(figure) {
this.embeddedElem = document.createElement("div"), this.embeddedElem.id = "embeddedElem",
this.embeddedElem.innerHTML = EmbeddedHTML.generateHtmlString();
document.getElementById("ddmenu").appendChild(this.embeddedElem), this.embeddedElem.addEventListener("click", (event => {
event.target == this.embeddedElem && this.remove();
}));
const zoomLevels = new WeakMap, backgroundElem = document.getElementById("embeddedBackgroundElem");
backgroundElem.addEventListener("wheel", (event => {
if (!0 === useCtrlForZoomSetting.value && !event.ctrlKey) return;
event.preventDefault(), zoomLevels.has(backgroundElem) || zoomLevels.set(backgroundElem, 1);
let zoomLevel = zoomLevels.get(backgroundElem);
if (zoomLevel = event.deltaY < 0 ? zoomLevel + .1 : Math.max(.1, zoomLevel - .1),
zoomLevels.set(backgroundElem, zoomLevel), !0 === alwaysZoomCenterSetting.value) {
const rect = backgroundElem.getBoundingClientRect(), mouseX = (event.clientX - rect.left) / rect.width * 100, mouseY = (event.clientY - rect.top) / rect.height * 100;
backgroundElem.style.transformOrigin = `${mouseX}% ${mouseY}%`;
} else backgroundElem.style.transformOrigin = "center";
const translateMatch = (backgroundElem.style.transform || "").match(/translate\([^)]+\)/), translateValue = translateMatch ? translateMatch[0] : "translate(-50%, 0%)";
backgroundElem.style.transform = `${translateValue} scale(${zoomLevel})`;
}));
const submissionContainer = document.getElementById("embeddedSubmissionContainer");
!0 === openInNewTabSetting.value && submissionContainer.setAttribute("target", "_blank"),
submissionContainer.addEventListener("click", (() => {
!0 === closeEmbedAfterOpenSetting.value && this.remove();
}));
const userLink = function(figcaption) {
if (figcaption) {
const infos = figcaption.querySelectorAll("i");
let userLink;
for (const info of Array.from(infos)) if (info.textContent.toLowerCase().includes("by")) {
const linkElem = info.parentNode.querySelector("a[href][title]");
linkElem && (userLink = linkElem.getAttribute("href"));
}
return userLink;
}
}(figure.querySelector("figcaption"));
if (userLink) {
const galleryLink = trimEnd(userLink, "/").replace("user", "gallery"), scrapsLink = trimEnd(userLink, "/").replace("user", "scraps");
if (!window.location.toString().includes(userLink) && !window.location.toString().includes(galleryLink) && !window.location.toString().includes(scrapsLink)) {
const openGalleryButton = document.getElementById("embeddedOpenGalleryButton");
openGalleryButton.style.display = "block", openGalleryButton.setAttribute("href", galleryLink),
!0 === openInNewTabSetting.value && openGalleryButton.setAttribute("target", "_blank"),
openGalleryButton.onclick = () => {
!0 === closeEmbedAfterOpenSetting.value && this.remove();
};
}
}
const link = figure.querySelector("a[href]").getAttribute("href"), openButton = document.getElementById("embeddedOpenButton");
openButton.setAttribute("href", link), !0 === openInNewTabSetting.value && openButton.setAttribute("target", "_blank"),
openButton.onclick = () => {
!0 === closeEmbedAfterOpenSetting.value && this.remove();
};
document.getElementById("embeddedCloseButton").onclick = () => this.remove();
document.getElementById("previewLoadingSpinnerContainer").onclick = () => {
this.previewLoadingSpinner.visible = !1;
};
}
async fillSubDocInfos(figure) {
const sid = figure.id.split("-")[1], ddmenu = document.getElementById("ddmenu"), doc = await requestHelper.SubmissionRequests.getSubmissionPage(sid);
if (doc) {
this.submissionImg = doc.getElementById("submissionImg");
const imgSrc = this.submissionImg.src;
let prevSrc = this.submissionImg.getAttribute("data-preview-src");
previewQualitySetting.value <= 2 ? prevSrc = prevSrc.replace("@600", "@200") : 3 === previewQualitySetting.value ? prevSrc = prevSrc.replace("@600", "@300") : 4 === previewQualitySetting.value && (prevSrc = prevSrc.replace("@600", "@400"));
const faImageViewer = new CustomImageViewer(imgSrc, prevSrc);
faImageViewer.faImage.id = "embeddedSubmissionImg", faImageViewer.faImagePreview.id = "previewSubmissionImg",
faImageViewer.faImage.className = faImageViewer.faImagePreview.className = "embeddedSubmissionImg",
faImageViewer.faImage.style.maxWidth = faImageViewer.faImagePreview.style.maxWidth = window.innerWidth - 40 + "px",
faImageViewer.faImage.style.maxHeight = faImageViewer.faImagePreview.style.maxHeight = window.innerHeight - ddmenu.clientHeight - 76 - 40 - 100 + "px",
faImageViewer.onImageLoadStart = () => {
this._imageLoaded = !1, this.loadingSpinner && (this.loadingSpinner.visible = !1);
}, faImageViewer.onImageLoad = () => {
this._imageLoaded = !0, this.loadingSpinner && !0 === this.loadingSpinner.visible && (this.loadingSpinner.visible = !1),
this.previewLoadingSpinner && !0 === this.previewLoadingSpinner.visible && (this.previewLoadingSpinner.visible = !1);
}, faImageViewer.onPreviewImageLoad = () => {
!1 === this._imageLoaded && (this.previewLoadingSpinner.visible = !0);
};
const submissionContainer = document.getElementById("embeddedSubmissionContainer");
faImageViewer.load(submissionContainer);
const url = doc.querySelector('meta[property="og:url"]').content;
submissionContainer.setAttribute("href", url);
const result = function(doc) {
const columnPage = doc.getElementById("columnpage"), buttons = columnPage.querySelector('div[class*="favorite-nav"').querySelectorAll('a[class*="button"][href]');
let favButton;
for (const button of Array.from(buttons)) button.textContent.toLowerCase().includes("fav") && (favButton = button);
if (favButton) {
return {
favKey: favButton.getAttribute("href").split("?key=")[1],
isFav: !favButton.getAttribute("href").toLowerCase().includes("unfav")
};
}
return null;
}(doc), favButton = document.getElementById("embeddedFavButton");
favButton.textContent = result.isFav ? "+Fav" : "-Fav", favButton.setAttribute("isFav", result.isFav),
favButton.setAttribute("key", result.favKey), favButton.addEventListener("click", (() => {
!1 === this.favRequestRunning && this.doFavRequest(sid);
}));
const downloadButton = document.getElementById("embeddedDownloadButton");
downloadButton.addEventListener("click", (() => {
if (!0 === this.downloadRequestRunning) return;
this.downloadRequestRunning = !0;
const loadingTextSpinner = new LoadingTextSpinner(downloadButton);
loadingTextSpinner.delay = loadingSpinSpeedFavSetting.value, loadingTextSpinner.visible = !0;
const iframe = document.createElement("iframe");
iframe.style.display = "none", iframe.src = this.submissionImg.src + "?eidownload",
iframe.addEventListener("load", (() => {
this.downloadRequestRunning = !1, loadingTextSpinner.visible = !1, setTimeout((() => iframe.parentNode.removeChild(iframe)), 100);
})), document.body.appendChild(iframe);
}));
}
}
async doFavRequest(sid) {
const favButton = document.getElementById("embeddedFavButton");
this.favRequestRunning = !0;
const loadingTextSpinner = new LoadingTextSpinner(favButton);
loadingTextSpinner.delay = loadingSpinSpeedFavSetting.value, loadingTextSpinner.visible = !0;
let favKey = favButton.getAttribute("key"), isFav = "true" == favButton.getAttribute("isFav");
!0 === isFav ? (favKey = await requestHelper.SubmissionRequests.favSubmission(sid, favKey),
loadingTextSpinner.visible = !1, favKey ? (favButton.setAttribute("key", favKey),
isFav = !1, favButton.setAttribute("isFav", isFav.toString()), favButton.textContent = "-Fav") : (favButton.textContent = "x",
setTimeout((() => favButton.textContent = "+Fav"), 1e3))) : (favKey = await requestHelper.SubmissionRequests.unfavSubmission(sid, favKey),
loadingTextSpinner.visible = !1, favKey ? (favButton.setAttribute("key", favKey),
isFav = !0, favButton.setAttribute("isFav", isFav.toString()), favButton.textContent = "+Fav") : (favButton.textContent = "x",
setTimeout((() => favButton.textContent = "-Fav"), 1e3))), this.favRequestRunning = !1;
}
}
async function addEmbedded() {
const nonEmbeddedFigures = document.querySelectorAll("figure:not([embedded])");
for (const figure of Array.from(nonEmbeddedFigures)) figure.setAttribute("embedded", "true"),
figure.addEventListener("click", (event => {
if (event instanceof MouseEvent && event.target instanceof HTMLElement && !event.ctrlKey && !event.target.id.includes("favbutton") && "checkbox" !== event.target.getAttribute("type")) {
if (event.target.getAttribute("href")) return;
event.preventDefault(), !EmbeddedImage.embeddedExists && figure instanceof HTMLElement && new EmbeddedImage(figure);
}
}));
}
function trimEnd(string, toRemove) {
return string.endsWith(toRemove) && (string = string.slice(0, -1)), string;
}
CustomSettings.name = "Extension Settings", CustomSettings.provider = "Midori's Script Settings",
CustomSettings.headerName = `${GM_info.script.name} Settings`;
const openInNewTabSetting = CustomSettings.newSetting("Open in new Tab", "Wether to open links in a new Tab or the current one.", SettingTypes.Boolean, "Open in new Tab", !0), loadingSpinSpeedFavSetting = CustomSettings.newSetting("Fav Loading Animation", "The duration that the loading animation, for faving a submission, takes for a full rotation in milliseconds.", SettingTypes.Number, "", 600), loadingSpinSpeedSetting = CustomSettings.newSetting("Embedded Loading Animation", "The duration that the loading animation of the Embedded element to load takes for a full rotation in milliseconds.", SettingTypes.Number, "", 1e3), closeEmbedAfterOpenSetting = CustomSettings.newSetting("Close Embed after open", "Wether to clos the current embedded Submission after it is opened in a new Tab (also for open Gallery).", SettingTypes.Boolean, "Close Embed after open", !0), useCtrlForZoomSetting = CustomSettings.newSetting("Use Ctrl for Zoom", "Wether the Ctrl-Key needs to be pressed while scrolling to zoom the Embedded Image.", SettingTypes.Boolean, "Use Ctrl for Zoom", !1), alwaysZoomCenterSetting = CustomSettings.newSetting("Zoom from Mouse Location", "Wether the Embedded Image should be zoomed from the Mouse Location. (Otherwise from Center)", SettingTypes.Boolean, "Zoom from Mouse Location", !0), previewQualitySetting = CustomSettings.newSetting("Preview Quality", "The quality of the preview image. Value range is 2-6. (Higher values can be slower)", SettingTypes.Number, "", 3);
CustomSettings.loadSettings();
const requestHelper = new FARequestHelper(2), matchList = new MatchList(CustomSettings);
if (matchList.matches = [ "net/browse", "net/user", "net/gallery", "net/search", "net/favorites", "net/scraps", "net/controls/favorites", "net/controls/submissions", "net/msg/submissions", "d.furaffinity.net" ],
matchList.runInIFrame = !0, matchList.hasMatch()) {
const page = new CustomPage("d.furaffinity.net", "eidownload");
let pageDownload = !1;
page.onopen = () => {
!function() {
let url = window.location.toString();
if (url.includes("?")) {
const parts = url.split("?");
url = parts[0];
}
const download = document.createElement("a");
download.href = url, download.download = url.substring(url.lastIndexOf("/") + 1),
download.style.display = "none", document.body.appendChild(download), download.click(),
document.body.removeChild(download), window.close();
}(), pageDownload = !0;
}, !1 === pageDownload && !1 === matchList.isWindowIFrame() && (new EmbeddedCSS,
addEmbedded(), window.addEventListener("updateEmbeddedEvent", (async () => {
await addEmbedded();
})));
}
})();