InoReader dynamic height of tiles in the card view

Makes cards' heights to be dynamic depending on image height

当前为 2024-04-16 提交的版本,查看 最新版本

// ==UserScript==
// @name         InoReader dynamic height of tiles in the card view
// @namespace    http://tampermonkey.net/
// @version      0.0.1
// @description  Makes cards' heights to be dynamic depending on image height
// @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/channel*
// @match        https://*.inoreader.com/teams*
// @match        https://*.inoreader.com/dashboard*
// @match        https://*.inoreader.com/pocket*
// @icon         https://inoreader.com/favicon.ico?v=8
// @license      MIT
// ==/UserScript==
// @ts-check

(function () {
    "use strict";

    document.head.insertAdjacentHTML("beforeend", `
<style>
    .tm_dynamic_height {
        height: auto !important;
    }
    .tm_remove_position_setting {
        position: unset !important;
    }
</style>`);

    const appConfig = {
        corsProxy: "https://corsproxy.io/?",
    };

    const appState = {
        readerPaneExists: false,
    };

    // 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";
    const querySelectorArticleContentWrapper = ".article_tile_content_wraper";
    const querySelectorArticleFooter = ".article_tile_footer";

    /**
     * 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 mutation of mutationsList) {
            if (mutation.type === "childList") {
                mutation.addedNodes.forEach(function (node) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        stylizeCardsInList(node);
                    }
                });
            }
        }
    };

    /**
     *
     * @param {Node} node
     * @returns {void}
     */
    function stylizeCardsInList(node) {
        const readerPane = document.body.querySelector("#reader_pane");
        if (readerPane) {
            if (!appState.readerPaneExists) {
                appState.readerPaneExists = 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) {
                    for (let mutation of mutationsList) {
                        if (mutation.type === "childList") {
                            mutation.addedNodes.forEach(function (node) {
                                if (node.nodeType === Node.ELEMENT_NODE) {
                                    if (appState.readerPaneExists) {
                                        setTimeout(() => {
                                            start(node);
                                        }, 500);
                                    }
                                }
                            });
                        }
                    }
                };

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

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

                // Start observing the target node for configured mutations
                tmObserverArticleList.observe(
                    readerPane,
                    mutationObserverLocalConfig
                );
            }
        } else {
            appState.readerPaneExists = false;
        }

        /**
         *
         * @param {Node} node
         */
        function start(node) {
            readerPane
                ?.querySelectorAll(`.ar:has(${querySelectorArticleContentWrapper})`)
                ?.forEach((element) => {
                    // @ts-ignore
                    const cardWidth = element.clientWidth ?? element.offsetWidth ?? element.scrollWidth;
                    // @ts-ignore
                    const cardHeight = element.clientHeight ?? element.offsetHeight ?? element.scrollHeight;

                    // 1. Set card height dynamic
                    setDynamicHeight(element);

                    // 2. Set cotnent wrapper height dynamic
                    const articleContentWrapperElement = element.querySelector(
                        querySelectorArticleContentWrapper
                    );
                    if (articleContentWrapperElement) {
                        setDynamicHeight(articleContentWrapperElement);
                    }

                    // 3. Remove position setting from article footer
                    const articleFooter = element.querySelector(
                        querySelectorArticleFooter
                    );
                    if (articleFooter) {
                        removePositionSetting(articleFooter);
                    }

                    // 4. Find image height
                    /**
                     * @type {HTMLDivElement | null}
                     */
                    const divImageElement = element.querySelector(
                        "a[href] > div[style*='background-image']"
                    );
                    if (!divImageElement) {
                        return;
                    }
                    const imageUrl = getImageLink(divImageElement);
                    if (!imageUrl) {
                        return;
                    }
                    const dimensions = getImageDimensions(imageUrl);

                    // 5. Set image height (and automaticlly the card height)
                    dimensions.then(([width, height]) => {
                        if (height > 0) {

                            const calculatedHeight = Math.round((cardWidth / width) * height);
                            /**
                             * @type {HTMLDivElement}
                             */
                            // @ts-ignore
                            const div = divImageElement;
                            if (calculatedHeight > cardHeight) {
                                div.style.height = `${calculatedHeight}px`;
                            }
                        }
                    });
                });
        }

        /**
         * 
         * @param {Element} element 
         * @returns {void}
         */
        function setDynamicHeight(element) {
            element.classList?.add("tm_dynamic_height");
        }

        /**
         * 
         * @param {Element} element 
         * @returns {void}
         */
        function removeDynamicHeight(element) {
            const div = element.querySelector("img");
            if (!div) {
                return;
            }
            div.classList?.remove("tm_dynamic_height");
        }

        /**
         * 
         * @param {Element} element
         * @returns {void}
         */
        function removePositionSetting(element) {
            element.classList?.add("tm_remove_position_setting");
        }

        /**
         * 
         * @param {Element} element
         * @returns {void}
         */
        function restorePositionSetting(element) {
            element.classList?.remove("tm_remove_position_setting");
        }

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

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

        /**
         * 
         * @param {string} url 
         * @returns {Promise<[number, number]>}
         */
        async function getImageDimensions(url) {
            const img = new Image();
            img.src = url;
            await img.decode();
            return [img.naturalWidth, img.naturalHeight];
        };
    }

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

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

    
    /**
     * 
     * @param {Function | void} mainFunction 
     * @param {number} delay 
     * @returns 
     */
    function debounce(mainFunction, delay) {
        // Declare a variable called 'timer' to store the timer ID
        /**
         * @type {number}
         */
        let timer;

        // Return an anonymous function that takes in any number of arguments
        /**
         * @param  {...any} args
         * @returns {void}
         */
        return function (...args) {
            // Clear the previous timer to prevent the execution of 'mainFunction'
            clearTimeout(timer);

            // Set a new timer that will execute 'mainFunction' after the specified delay
            timer = setTimeout(() => {
                // @ts-ignore
                mainFunction(...args);
            }, delay);
        };
    }
})();