Skip to content

Instantly share code, notes, and snippets.

@Kenya-West
Last active April 29, 2024 13:13
Show Gist options
  • Save Kenya-West/9c0ca555d4e229a56dbdf4d02b2b07ef to your computer and use it in GitHub Desktop.
Save Kenya-West/9c0ca555d4e229a56dbdf4d02b2b07ef to your computer and use it in GitHub Desktop.
InoReader dynamic height of tiles in the card view
// ==UserScript==
// @name InoReader dynamic height of tiles in the card view
// @namespace http://tampermonkey.net/
// @version 0.0.4
// @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/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_dynamic_height {
height: auto !important;
}
.tm_remove_position_setting {
position: unset !important;
}
</style>`);
const appConfig = {
corsProxy: "https://corsproxy.io/?",
};
const appState = {
tmObserverArticleListLinked: 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) {
/**
* @type {MutationObserver | undefined}
*/
let tmObserverArticleList;
const readerPane = document.body.querySelector("#reader_pane");
if (readerPane) {
if (!appState.tmObserverArticleListLinked) {
appState.tmObserverArticleListLinked = 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.tmObserverArticleListLinked) {
setTimeout(() => {
start(node);
}, 3500);
// the second attempt is needed because some images or videos are loaded after the first attempt
setTimeout(() => {
start(node);
}, 10000);
}
}
});
}
}
};
// 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
tmObserverArticleList = new MutationObserver(callback);
// Start observing the target node for configured mutations
tmObserverArticleList.observe(
readerPane,
mutationObserverLocalConfig
);
}
} else {
appState.tmObserverArticleListLinked = false;
tmObserverArticleList?.disconnect();
}
/**
*
* @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]);
};
}
// 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);
};
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment