FA Embedded Image Viewer

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

目前為 2024-11-16 提交的版本,檢視 最新版本

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        FA Embedded Image Viewer
// @namespace   Violentmonkey Scripts
// @match       *://*.furaffinity.net/*
// @require     https://update.greatest.deepsurf.us/scripts/475041/1267274/Furaffinity-Custom-Settings.js
// @require     https://update.greatest.deepsurf.us/scripts/483952/1329447/Furaffinity-Request-Helper.js
// @require     https://update.greatest.deepsurf.us/scripts/485153/1316289/Furaffinity-Loading-Animations.js
// @require     https://update.greatest.deepsurf.us/scripts/476762/1318215/Furaffinity-Custom-Pages.js
// @require     https://update.greatest.deepsurf.us/scripts/485827/1326313/Furaffinity-Match-List.js
// @require     https://update.greatest.deepsurf.us/scripts/492931/1363921/Furaffinity-Submission-Image-Viewer.js
// @grant       none
// @version     2.2.0
// @author      Midori Dragon
// @description Embedds 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
// @homepageURL https://greatest.deepsurf.us/de/scripts/458971-embedded-image-viewer
// @supportURL  https://greatest.deepsurf.us/de/scripts/458971-embedded-image-viewer/feedback
// @license     MIT
// ==/UserScript==

// jshint esversion: 8

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", "Sets wether to open links in a new Tab or the current one.", SettingTypes.Boolean, "Open in new Tab", true);
const loadingSpinSpeedFavSetting = CustomSettings.newSetting("Fav Loading Animation", "Sets the duration that the loading animation, for faving a submission, takes for a full rotation in milliseconds.", SettingTypes.Number, "", 600);
const loadingSpinSpeedSetting = CustomSettings.newSetting("Embedded Loading Animation", "Sets the duration that the loading animation of the Embedded element to load takes for a full rotation in milliseconds.", SettingTypes.Number, "", 1000);
const closeEmbedAfterOpenSetting = CustomSettings.newSetting("Close Embed after open", "Closes the current embedded Submission after it is opened in a new Tab (also for open Gallery)", SettingTypes.Boolean, "Close Embed after open", true);
const useCtrlForZoomSetting = CustomSettings.newSetting("Use Ctrl for Zoom", "Whether the Ctrl-Key needs to be pressed while scrolling to zoom the Embedded Image", SettingTypes.Boolean, "Use Ctrl for Zoom", false);
CustomSettings.loadSettings();

const matchList = new MatchList(CustomSettings);
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 = true;
if (!matchList.hasMatch())
    return;

const page = new CustomPage("d.furaffinity.net", "eidownload");
page.onopen = (data) => {
    downloadImage();
    return;
};

if (matchList.isWindowIFrame() == true)
    return;

const requestHelper = new FARequestHelper(2);

class EmbeddedImage {
    constructor(figure) {
        this._previewLoaded;
        this._imageLoaded;

        this.embeddedElem;
        this.backgroundElem;
        this.submissionContainer;
        this.submissionImg;
        this.buttonsContainer;
        this.previewLoadingSpinnerContainer;
        this.favButton;
        this.downloadButton;
        this.closeButton;

        this.favRequestRunning = false;
        this.downloadRequestRunning = false;

        this._onRemoveAction;

        this.createStyle();
        this.createElements(figure);

        this.loadingSpinner = new LoadingSpinner(this.submissionContainer);
        this.loadingSpinner.delay = loadingSpinSpeedSetting.value;
        this.loadingSpinner.spinnerThickness = 6;
        this.loadingSpinner.visible = true;

        this.previewLoadingSpinner = new LoadingSpinner(this.previewLoadingSpinnerContainer);
        this.previewLoadingSpinner.delay = loadingSpinSpeedSetting.value;
        this.previewLoadingSpinner.spinnerThickness = 4;
        this.previewLoadingSpinner.size = 40;

        this.fillSubDocInfos(figure);
    }

    createStyle() {
        if (document.getElementById("embeddedStyle")) return;
        const style = document.createElement("style");
        style.id = "embeddedStyle";
        style.type = "text/css";
        style.innerHTML = `
            #embeddedElem {
                position: fixed;
                width: 100vw;
                height: 100vh;
                max-width: 1850px;
                z-index: 999999;
                background: rgba(30,33,38,.65);
            }
            #embeddedBackgroundElem {
                position: fixed;
                display: flex;
                flex-direction: column;
                left: 50%;
                transform: translate(-50%, 0%);
                margin-top: 20px;
                padding: 20px;
                background: rgba(30,33,38,.90);
                border-radius: 10px;
            }
            .embeddedSubmissionImg {
                max-width: inherit;
                max-height: inherit;
                border-radius: 10px;
                user-select: none;
            }
            #embeddedButtonsContainer {
                position: relative;
                margin-top: 20px;
                margin-bottom: 20px;
                margin-left: 20px;
            }
            #embeddedButtonsWrapper {
                display: flex;
                justify-content: center;
                align-items: center;
            }
            #previewLoadingSpinnerContainer {
                position: absolute;
                top: 50%;
                right: 0;
                transform: translateY(-50%);
            }
            .embeddedButton {
                margin-left: 4px;
                margin-right: 4px;
                user-select: none;
            }
        `;
        document.head.appendChild(style);
    }

    onRemove(action) {
        this._onRemoveAction = action;
    }

    remove() {
        this.embeddedElem.parentNode.removeChild(this.embeddedElem);
        if (this._onRemoveAction)
            this._onRemoveAction();
    }

    createElements(figure) {
        this.embeddedElem = document.createElement("div");
        this.embeddedElem.id = "embeddedElem";
        this.embeddedElem.onclick = (event) => {
            if (event.target == this.embeddedElem)
                this.remove();
        };

        const zoomLevels = new WeakMap();
        this.backgroundElem = document.createElement("div");
        this.backgroundElem.id = "embeddedBackgroundElem";
        this.backgroundElem.addEventListener('wheel', (event) => {
            if (useCtrlForZoomSetting.value === true && !event.ctrlKey) {
                return;
            }
            event.preventDefault(); // Prevent page scroll

            // Initialize zoom level for this image if not already set
            if (!zoomLevels.has(this.backgroundElem)) {
                zoomLevels.set(this.backgroundElem, 1);
            }

            // Get the current zoom level
            let zoomLevel = zoomLevels.get(this.backgroundElem);

            // Adjust zoom level based on scroll direction
            if (event.deltaY < 0) {
                zoomLevel += 0.1; // Zoom in
            } else {
                zoomLevel = Math.max(0.1, zoomLevel - 0.1); // Zoom out, with a minimum limit
            }

            // Save the updated zoom level
            zoomLevels.set(this.backgroundElem, zoomLevel);

            // Calculate mouse position relative to the image
            const rect = this.backgroundElem.getBoundingClientRect();
            const mouseX = ((event.clientX - rect.left) / rect.width) * 100;
            const mouseY = ((event.clientY - rect.top) / rect.height) * 100;

            // Get the current transform value
            const existingTransform = this.backgroundElem.style.transform || '';

            // Extract any existing translate transform
            const translateMatch = existingTransform.match(/translate\([^)]+\)/);
            const translateValue = translateMatch ? translateMatch[0] : 'translate(-50%, 0%)';

            // Apply the combined transform with scale
            this.backgroundElem.style.transform = `${translateValue} scale(${zoomLevel})`;
            this.backgroundElem.style.transformOrigin = `${mouseX}% ${mouseY}%`;
        });
        notClosingElemsArr.push(this.backgroundElem.id);

        this.submissionContainer = document.createElement("a");
        this.submissionContainer.id = "embeddedSubmissionContainer";
        if (openInNewTabSetting.value == true)
            this.submissionContainer.target = "_blank";
        this.submissionContainer.onclick = () => {
            if (closeEmbedAfterOpenSetting.value == true)
                this.remove();
        };
        notClosingElemsArr.push(this.submissionContainer.id);

        this.backgroundElem.appendChild(this.submissionContainer);

        this.buttonsContainer = document.createElement("div");
        this.buttonsContainer.id = "embeddedButtonsContainer";
        notClosingElemsArr.push(this.buttonsContainer.id);

        this.buttonsWrapper = document.createElement("div");
        this.buttonsWrapper.id = "embeddedButtonsWrapper";
        notClosingElemsArr.push(this.buttonsWrapper.id);
        this.buttonsContainer.appendChild(this.buttonsWrapper);

        this.favButton = document.createElement("a");
        this.favButton.id = "embeddedFavButton";
        notClosingElemsArr.push(this.favButton.id);
        this.favButton.type = "button";
        this.favButton.className = "embeddedButton button standard mobile-fix";
        this.favButton.textContent = "⠀⠀";
        this.buttonsWrapper.appendChild(this.favButton);

        this.downloadButton = document.createElement("a");
        this.downloadButton.id = "embeddedDownloadButton";
        notClosingElemsArr.push(this.downloadButton.id);
        this.downloadButton.type = "button";
        this.downloadButton.className = "embeddedButton button standard mobile-fix";
        this.downloadButton.textContent = "Download";
        this.buttonsWrapper.appendChild(this.downloadButton);

        const userLink = getByLinkFromFigcaption(figure.querySelector("figcaption"));
        if (userLink) {
            const galleryLink = trimEnd(userLink, "/").replace("user", "gallery");
            const scrapsLink = trimEnd(userLink, "/").replace("user", "scraps");
            if (!window.location.toString().includes(userLink) && !window.location.toString().includes(galleryLink) && !window.location.toString().includes(scrapsLink)) {
                this.openGalleryButton = document.createElement("a");
                this.openGalleryButton.id = "embeddedOpenGalleryButton";
                notClosingElemsArr.push(this.openGalleryButton.id);
                this.openGalleryButton.type = "button";
                this.openGalleryButton.className = "embeddedButton button standard mobile-fix";
                this.openGalleryButton.textContent = "Open Gallery";
                this.openGalleryButton.href = galleryLink;
                if (openInNewTabSetting.value == true)
                    this.openGalleryButton.target = "_blank";
                this.openGalleryButton.onclick = () => {
                    if (closeEmbedAfterOpenSetting.value == true)
                        this.remove();
                };
                this.buttonsWrapper.appendChild(this.openGalleryButton);
            }
        }

        this.openButton = document.createElement("a");
        this.openButton.id = "embeddedOpenButton";
        notClosingElemsArr.push(this.openButton.id);
        this.openButton.type = "button";
        this.openButton.className = "embeddedButton button standard mobile-fix";
        this.openButton.textContent = "Open";
        const link = figure.querySelector("a[href]");
        this.openButton.href = link;
        if (openInNewTabSetting.value == true)
            this.openButton.target = "_blank";
        this.openButton.onclick = () => {
            if (closeEmbedAfterOpenSetting.value == true)
                this.remove();
        };
        this.buttonsWrapper.appendChild(this.openButton);

        this.closeButton = document.createElement("a");
        this.closeButton.id = "embeddedCloseButton";
        notClosingElemsArr.push(this.closeButton.id);
        this.closeButton.type = "button";
        this.closeButton.className = "embeddedButton button standard mobile-fix";
        this.closeButton.textContent = "Close";
        this.closeButton.onclick = () => this.remove();
        this.buttonsWrapper.appendChild(this.closeButton);

        this.previewLoadingSpinnerContainer = document.createElement("div");
        this.previewLoadingSpinnerContainer.id = "previewLoadingSpinnerContainer";
        notClosingElemsArr.push(this.previewLoadingSpinnerContainer.id);
        this.previewLoadingSpinnerContainer.onclick = () => {
            this.previewLoadingSpinner.visible = false;
        };
        this.buttonsContainer.appendChild(this.previewLoadingSpinnerContainer);

        this.backgroundElem.appendChild(this.buttonsContainer);

        this.embeddedElem.appendChild(this.backgroundElem);

        const ddmenu = document.getElementById("ddmenu");
        ddmenu.appendChild(this.embeddedElem);
    }

    async fillSubDocInfos(figure) {
        const sid = figure.id.split("-")[1];
        const ddmenu = document.getElementById("ddmenu");
        const doc = await requestHelper.SubmissionRequests.getSubmissionPage(sid);
        if (doc) {
            this.submissionImg = doc.getElementById("submissionImg");
            const imgSrc = this.submissionImg.src;
            const prevSrc = this.submissionImg.getAttribute("data-preview-src");
            const prevPrevSrc = prevSrc.replace("@600", "@300");

            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 - 20 * 2 + "px";
            faImageViewer.faImage.style.maxHeight = faImageViewer.faImagePreview.style.maxHeight = window.innerHeight - ddmenu.clientHeight - 38 * 2 - 20 * 2 - 100 + "px";
            faImageViewer.onImageLoadStart = () => {
                this._previewLoaded = false;
                this._imageLoaded = false;
                if (this.loadingSpinner)
                    this.loadingSpinner.visible = false;
            };
            faImageViewer.onImageLoad = () => {
                this._imageLoaded = true;
                if (this.loadingSpinner && this.loadingSpinner.visible === true)
                    this.loadingSpinner.visible = false;
                if (this.previewLoadingSpinner && this.previewLoadingSpinner.visible === true)
                    this.previewLoadingSpinner.visible = false;
            };
            faImageViewer.onPreviewImageLoad = () => {
                this._previewLoaded = true;
                if (this._imageLoaded === false)
                    this.previewLoadingSpinner.visible = true;
            };
            faImageViewer.load(this.submissionContainer);

            this.submissionContainer.href = doc.querySelector('meta[property="og:url"]').content;

            const result = getFavKey(doc);
            this.favButton.textContent = result.isFav ? "+Fav" : "-Fav";
            this.favButton.setAttribute("isFav", result.isFav);
            this.favButton.setAttribute("key", result.favKey);
            this.favButton.onclick = () => {
                if (this.favRequestRunning == false)
                    this.doFavRequest(sid);
            };

            this.downloadButton.onclick = () => {
                if (this.downloadRequestRunning == true)
                    return;
                this.downloadRequestRunning = true;
                const loadingTextSpinner = new LoadingTextSpinner(this.downloadButton);
                loadingTextSpinner.delay = loadingSpinSpeedFavSetting.value;
                loadingTextSpinner.visible = true;
                const iframe = document.createElement("iframe");
                iframe.style.display = "none";
                iframe.src = this.submissionImg.src + "?eidownload";
                iframe.onload = () => {
                    this.downloadRequestRunning = false;
                    loadingTextSpinner.visible = false;
                    setTimeout(() => iframe.parentNode.removeChild(iframe), 100);
                };
                document.body.appendChild(iframe);
            };
        }
    }

    async doFavRequest(sid) {
        this.favRequestRunning = true;
        const loadingTextSpinner = new LoadingTextSpinner(this.favButton);
        loadingTextSpinner.delay = loadingSpinSpeedFavSetting.value;
        loadingTextSpinner.visible = true;
        let favKey = this.favButton.getAttribute("key");
        let isFav = this.favButton.getAttribute("isFav");
        if (isFav == "true") {
            favKey = await requestHelper.SubmissionRequests.favSubmission(sid, favKey);
            loadingTextSpinner.visible = false;
            if (favKey) {
                this.favButton.setAttribute("key", favKey);
                isFav = false;
                this.favButton.setAttribute("isFav", isFav);
                this.favButton.textContent = "-Fav";
            } else {
                this.favButton.textContent = "x";
                setTimeout(() => this.favButton.textContent = "+Fav", 1000);
            }
        } else {
            favKey = await requestHelper.SubmissionRequests.unfavSubmission(sid, favKey);
            loadingTextSpinner.visible = false;
            if (favKey) {
                this.favButton.setAttribute("key", favKey);
                isFav = true;
                this.favButton.setAttribute("isFav", isFav);
                this.favButton.textContent = "+Fav";
            } else {
                this.favButton.textContent = "x";
                setTimeout(() => this.favButton.textContent = "-Fav", 1000);
            }
        }
        this.favRequestRunning = false;
    }
}

function getByLinkFromFigcaption(figcaption) {
    if (figcaption) {
        const infos = figcaption.querySelectorAll("i");
        let userLink;
        for (const info of infos) {
            if (info.textContent.toLowerCase().includes("by")) {
                const linkElem = info.parentNode.querySelector("a[href][title]");
                if (linkElem)
                    userLink = linkElem.href;
            }
        }
        return userLink;
    }
}

function getFavKey(doc) {
    const columnPage = doc.getElementById("columnpage");
    const navbar = columnPage.querySelector('div[class*="favorite-nav"');
    const buttons = navbar.querySelectorAll('a[class*="button"][href]');
    let favButton;
    for (const button of buttons) {
        if (button.textContent.toLowerCase().includes("fav"))
            favButton = button;
    }

    if (favButton) {
        const favKey = favButton.href.split("?key=")[1];
        const isFav = !favButton.href.toLowerCase().includes("unfav");
        return { favKey, isFav };
    }
}

let isShowing = false;
let notClosingElemsArr = [];
let embeddedImage;

addEmbedded();
window.updateEmbedded = addEmbedded;

document.addEventListener("click", (event) => {
    if (event.target.parentNode instanceof HTMLDocument && embeddedImage)
        embeddedImage.remove();
});

async function addEmbedded() {
    for (const figure of document.querySelectorAll('figure:not([embedded])')) {
        figure.setAttribute('embedded', true);
        figure.addEventListener("click", function (event) {
            if (!event.ctrlKey && !event.target.id.includes("favbutton") && event.target.type != "checkbox") {
                if (event.target.href)
                    return;
                else
                    event.preventDefault();
                if (!isShowing)
                    showImage(figure);
            }
        });
    }
}

async function showImage(figure) {
    isShowing = true;
    embeddedImage = new EmbeddedImage(figure);
    embeddedImage.onRemove(() => {
        embeddedImage = null;
        isShowing = false;
    });
}

function downloadImage() {
    console.log("Embedded Image Viewer downloading Image...");
    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();
}

function trimEnd(string, toRemove) {
    if (string.endsWith(toRemove))
        string = string.slice(0, -1);
    return string;
}