FA Embedded Image Viewer

Embeds the clicked Image on the Current Site, so you can view it without loading the submission Page

As of 2024-11-18. See the latest version.

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