InoReader copy cover image

Copy cover image of the post you selected in article list view

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         InoReader copy cover image
// @namespace    http://tampermonkey.net/
// @version      0.0.2
// @description  Copy cover image of the post you selected in article list view
// @author       Kenya-West
// @match        https://*.inoreader.com/feed*
// @match        https://*.inoreader.com/article*
// @match        https://*.inoreader.com/folder*
// @match        https://*.inoreader.com/starred*
// @match        https://*.inoreader.com/library*
// @match        https://*.inoreader.com/dashboard*
// @match        https://*.inoreader.com/web_pages*
// @match        https://*.inoreader.com/trending*
// @match        https://*.inoreader.com/commented*
// @match        https://*.inoreader.com/recent*
// @match        https://*.inoreader.com/search*
// @match        https://*.inoreader.com/channel*
// @match        https://*.inoreader.com/teams*
// @match        https://*.inoreader.com/dashboard*
// @match        https://*.inoreader.com/pocket*
// @match        https://*.inoreader.com/liked*
// @match        https://*.inoreader.com/tags*
// @icon         https://inoreader.com/favicon.ico?v=8
// @license      MIT
// ==/UserScript==
// @ts-check

(function () {
    "use strict";

    document.head.insertAdjacentHTML(
        "beforeend",
        `
<style>
    .tm_copy_image_button {
        display: inline-block;
        cursor: pointer;
        position: absolute;
        right: 0.5rem;
        top: -2rem;
        background-color: rgba(0, 0, 0, 0.3);
        color: white;
        font-family: 'Inoreader-UI-Icons-Font' !important;
        font-size: 1.5rem;
        padding: 0.1rem;
        border-radius: 50%;
        margin-left: 0.5rem;
        transition: background-color 0.3s;
    }
    .tm_copy_image_button:hover {
        background-color: rgba(0, 0, 0, 0.7);
        transition: background-color 0.3s;
    }
    .tm_copy_image_button:active {
        background-color: rgba(0, 0, 0, 0.9);
        transition: background-color 0.3s;
    }
    .tm_copy_image_button::before {
        content: "\\ea11";
    }
    .tm_copy_image_button__success::before {
        content: "\\e976";
    }
</style>`
    );

    /**
     * @typedef {Object} appConfig
     * @property {Array<{
     *     prefixUrl: string,
     *     corsType: "direct" | "corsSh" | "corsAnywhere" | "corsFlare",
     *     token?: string,
     *     hidden?: boolean
     * }>} corsProxies
     */
    const appConfig = {
    };

    /**
     * Represents the application state.
     * @typedef {Object} AppState
     * @property {boolean} readerPaneMutationObserverLinked - Indicates whether the reader pane mutation observer is linked.
     * @property {boolean} articleViewOpened - Indicates whether the article view is opened.
     * @property {Object} copyBadge - Represents the currently playing video.
     * @property {HTMLDivElement | null} copyBadge.currentVideoElement - The current video element being played.
     * @property {function} copyBadge.set - Sets the current video element and pauses the previous one.
     * @property {function} copyBadge.get - Retrieves the current video element.
     */
    const appState = {
        readerPaneMutationObserverLinked: false,
        articleViewOpened: false,
        copyBadge: {
            /**
             * Represents the currently playing video.
             * @type {HTMLDivElement | null}
             */
            currentCopyBadgeElement: null,
            /**
             *
             * @param {HTMLDivElement | null} badge
             */
            set: (badge) => {
                const previousCopyBadge = appState.copyBadge.currentCopyBadgeElement;
                if (previousCopyBadge?.isConnected) {
                    appState.copyBadge.currentCopyBadgeElement?.parentElement?.removeChild(previousCopyBadge);
                }
                appState.copyBadge.currentCopyBadgeElement = badge;
            },
            /**
             *
             * @returns {HTMLDivElement | null}
             */
            get: () => {
                return appState.copyBadge.currentCopyBadgeElement;
            },
            disconnect: () => {
                if (appState.copyBadge.currentCopyBadgeElement?.isConnected) {
                    appState.copyBadge.currentCopyBadgeElement?.parentElement?.removeChild(appState.copyBadge.currentCopyBadgeElement);
                    appState.copyBadge.currentCopyBadgeElement = null;
                }
            },
        },
    };

    // Select the node that will be observed for mutations
    const targetNode = document.body;

    // Options for the observer (which mutations to observe)
    const mutationObserverGlobalConfig = {
        attributes: false,
        childList: true,
        subtree: true,
    };

    const querySelectorPathArticleRoot = ".article_full_contents .article_content";

    /**
     * Callback function to execute when mutations are observed
     * @param {MutationRecord[]} mutationsList - List of mutations observed
     * @param {MutationObserver} observer - The MutationObserver instance
     */
    const callback = function (mutationsList, observer) {
        for (let i = 0; i < mutationsList.length; i++) {
            if (mutationsList[i].type === "childList") {
                mutationsList[i].addedNodes.forEach(function (node) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        setCopyIconInArticleList(node);
                    }
                });
            }
        }
    };

    //
    //
    // FIRST PART - RESTORE IMAGES IN ARTICLE LIST
    //
    //
    //

    /**
     *
     * @param {Node} node
     * @returns {void}
     */
    function setCopyIconInArticleList(node) {
        /**
         * @type {MutationObserver | undefined}
         */
        let tmObserverImageRestoreReaderPane;
        const readerPane = document.body.querySelector("#reader_pane");
        if (readerPane) {
            if (!appState.readerPaneMutationObserverLinked) {
                appState.readerPaneMutationObserverLinked = true;

                /**
                 * Callback function to execute when mutations are observed
                 * @param {MutationRecord[]} mutationsList - List of mutations observed
                 * @param {MutationObserver} observer - The MutationObserver instance
                 */
                const callback = function (mutationsList, observer) {
                    // filter mutations by having id on target and to have only unique id attribute values
                    let filteredMutations = mutationsList
                        // @ts-ignore
                        .filter((mutation) => mutation.target?.id.includes("article_"))
                        // @ts-ignore
                        .filter((mutation, index, self) => self.findIndex((t) => t.target?.id === mutation.target?.id) === index);

                    if (filteredMutations.length === 2) {
                        // check to have only two mutations: one that has .article_current class and one should not
                        const firstMutation = filteredMutations[0];
                        const secondMutation = filteredMutations[1];
                        // sort by abscence of .article_current class
                        filteredMutations = [firstMutation, secondMutation].sort((a, b) => {
                            // @ts-ignore
                            return a.target?.classList?.contains("article_current") ? 1 : -1;
                        });

                        // @ts-ignore
                        if (firstMutation.target?.classList?.contains("article_current") && !secondMutation.target?.classList?.contains("article_current")) {
                            filteredMutations = [];
                        }
                    }

                    for (let mutation of filteredMutations) {
                        if (mutation.type === "attributes") {
                            if (mutation.attributeName === "class") {
                                /**
                                 * @type {HTMLDivElement}
                                 */
                                // @ts-ignore
                                const target = mutation.target;

                                if (
                                    target.classList.contains("article_current") &&
                                    target.querySelector(".article_tile_content_wraper .article_tile_picture")
                                ) {
                                    // тут
                                    const imageElement = getImageElement(target);
                                    if (imageElement) {
                                        const imageUrl = getImageLink(imageElement);
                                        if (imageUrl) {
                                            const button = createButtonElement(imageUrl);
                                            placeButton(target, button);
                                            appState.copyBadge.set(button);
                                        }
                                    }
                                } else if (
                                    !target.classList.contains("article_current") &&
                                    target.querySelector(".article_tile_content_wraper .article_tile_picture")
                                ) {
                                    // тут если снято выделение
                                }

                                /**
                                 *
                                 * @param {Node & HTMLDivElement} node
                                 * @returns {HTMLDivElement | null}
                                 */
                                function getImageElement(node) {
                                    const nodeElement = node;
                                    /**
                                     * @type {HTMLDivElement | null}
                                     */
                                    const divImageElement = nodeElement.querySelector("a[href] > div[style*='background-image']");
                                    return divImageElement ?? null;
                                }

                                /**
                                 *
                                 * @param {HTMLDivElement} div
                                 */
                                function getImageLink(div) {
                                    const backgroundImageUrl = div?.style.backgroundImage;
                                    return commonGetUrlFromBackgroundImage(backgroundImageUrl);
                                }

                                /**
                                 *
                                 * @param {string} imageUrl
                                 * @returns {HTMLDivElement}
                                 */
                                function createButtonElement(imageUrl) {
                                    const button = document.createElement("div");
                                    button.className = "tm_copy_image_button";
                                    button.title = "Copy image to clipboard";
                                    button.addEventListener("click", () => {
                                        copyImage(imageUrl);
                                    });
                                    return button;
                                }

                                /**
                                 *
                                 * @param {HTMLDivElement} article
                                 * @param {HTMLDivElement} buttonElement
                                 */
                                function placeButton(article, buttonElement) {
                                    if (article) {
                                        article.appendChild(buttonElement);
                                    } else {
                                        console.error("Article was not found. Copy button has not been placed");
                                    }
                                }

                                /**
                                 *
                                 * @param {string} imageLink
                                 */
                                function copyImage(imageLink) {
                                    const img = new Image();
                                    img.crossOrigin = "Anonymous"; // This enables CORS
                                    const c = document.createElement("canvas");
                                    const ctx = c.getContext("2d");

                                    /**
                                     * @param {string} path
                                     * @param {{ (imgBlob: any): void; (arg0: any): void; }} func
                                     */
                                    function setCanvasImage(path, func) {
                                        img.onload = function () {
                                            // @ts-ignore
                                            c.width = this.naturalWidth;
                                            // @ts-ignore
                                            c.height = this.naturalHeight;
                                            // @ts-ignore
                                            ctx.drawImage(this, 0, 0);
                                            c.toBlob((/** @type {any} */ blob) => {
                                                func(blob);
                                            }, "image/png");
                                        };
                                        img.src = path;
                                    }

                                    setCanvasImage(imageLink, (/** @type {any} */ imgBlob) => {
                                        navigator.clipboard
                                            .write([new ClipboardItem({ "image/png": imgBlob })])
                                            .then((e) => {
                                                setSuccessIcon();
                                            })
                                            .catch((e) => {
                                                console.error(e);
                                                alert(
                                                    `Failed to copy image to clipboard. This feature may not supported in your browser, or something happened with image. Please try to save it manually. Error: ${
                                                        e.message ?? e.body ?? e.toString() ?? e.name ?? e.constructor.name ?? e.constructor.toString()
                                                    }`
                                                );
                                            });
                                    });
                                }

                                function setSuccessIcon() {
                                    appState.copyBadge.get()?.classList.add("tm_copy_image_button__success");
                                }
                            }
                        }
                    }
                };

                // Options for the observer (which mutations to observe)
                const mutationObserverLocalConfig = {
                    attributes: true,
                    attributeFilter: ["class"],
                    childList: false,
                    subtree: true,
                };

                // Create an observer instance linked to the callback function
                tmObserverImageRestoreReaderPane = new MutationObserver(callback);

                // Start observing the target node for configured mutations
                tmObserverImageRestoreReaderPane.observe(readerPane, mutationObserverLocalConfig);
            }
        } else {
            appState.readerPaneMutationObserverLinked = false;
            tmObserverImageRestoreReaderPane?.disconnect();
        }
    }

    /**
     *
     * @param {string} backgroundImageUrl
     * @returns {string | undefined}
     */
    function commonGetUrlFromBackgroundImage(backgroundImageUrl) {
        /**
         * @type {string | undefined}
         */
        let imageUrl;
        try {
            imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
        } catch (error) {
            imageUrl = backgroundImageUrl?.slice(5, -2);
        }

        if (!imageUrl || imageUrl == "undefined") {
            return;
        }

        if (!imageUrl?.startsWith("http")) {
            console.error(`The image could not be parsed. Image URL: ${imageUrl}`);
            return;
        }
        return imageUrl;
    }

    // Create an observer instance linked to the callback function
    const tmObserverImageRestore = new MutationObserver(callback);

    // Start observing the target node for configured mutations
    tmObserverImageRestore.observe(targetNode, mutationObserverGlobalConfig);
})();