Greasy Fork

InoReader dynamic height of tiles in the card view

Makes cards' heights to be dynamic depending on image height

// ==UserScript==
// @name         InoReader dynamic height of tiles in the card view
// @namespace    http://tampermonkey.net/
// @version      0.1.0
// @description  Makes cards' heights to be dynamic depending on image height
// @author       Kenya-West
// @match        https://*.inoreader.com/*
// @icon         https://inoreader.com/favicon.ico?v=8
// @license      MIT
// ==/UserScript==
// @ts-check

(function () {
    "use strict";

    document.addEventListener('tm_inoreader-viewing-api-for-userscripts_articleAdded', (e) => {
        const { element } = e.detail?.details;

        setTimeout(() => {
            start(element);
        }, 3500);
        // the second attempt is needed because some images or videos are loaded after the first attempt
        setTimeout(() => {
            start(element);
        }, 10000);

    });

    const querySelectorPathArticleRoot =
        ".article_full_contents .article_content";
    const querySelectorArticleContentWrapper = ".article_tile_content_wraper";
    const querySelectorArticleFooter = ".article_tile_footer";

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

    /**
     *
     * @param {Node} node
     */
    function start(node) {
        /**
         * @type {Node & HTMLDivElement}
         */
        // @ts-ignore
        const element = node;
        if (
            element?.hasChildNodes() &&
            element?.id?.includes("article_") &&
            element?.classList.contains("ar") &&
            !element?.classList.contains("tm_dynamic_height")
        ) {
            // @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] > .article_tile_picture[style*='background-image']"
            );
            if (!divImageElement) {
                return;
            }
            const imageUrl = getImageLink(divImageElement);
            if (!imageUrl) {
                return;
            }
            const dimensions = getImageDimensions(imageUrl);

            // 5. Set image height (and - automatically - the card height)
            dimensions.then(([width, height]) => {
                if (height > 0) {
                    const calculatedHeight = Math.round(
                        (cardWidth / width) * height
                    );
                    const pictureOldHeight =
                        (divImageElement.clientHeight ??
                            divImageElement.offsetHeight ??
                            divImageElement.scrollHeight) ||
                        cardHeight;
                    /**
                     * @type {HTMLDivElement}
                     */
                    // @ts-ignore
                    const div = divImageElement;
                    if (calculatedHeight > pictureOldHeight) {
                        div.style.height = `${calculatedHeight}px`;
                    }

                    // 5.1. Set card class to `.tm_dynamic_height` to not process it again next time
                    element.classList?.add("tm_dynamic_height");
                }
            });
        }
    }

    /**
     * 
     * @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 || imageUrl == "undefined") {
            return null;
        }

        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;
        try {
            await img.decode();
        } catch (error) {
            return Promise.reject(error);
        }
        return Promise.resolve([img.width, img.height]);
    };
})();