InoReader dynamic height of tiles in the card view

Makes cards' heights to be dynamic depending on image height

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

  1. // ==UserScript==
  2. // @name InoReader dynamic height of tiles in the card view
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.0.4
  5. // @description Makes cards' heights to be dynamic depending on image height
  6. // @author Kenya-West
  7. // @match https://*.inoreader.com/feed*
  8. // @match https://*.inoreader.com/article*
  9. // @match https://*.inoreader.com/folder*
  10. // @match https://*.inoreader.com/starred*
  11. // @match https://*.inoreader.com/library*
  12. // @match https://*.inoreader.com/dashboard*
  13. // @match https://*.inoreader.com/web_pages*
  14. // @match https://*.inoreader.com/trending*
  15. // @match https://*.inoreader.com/commented*
  16. // @match https://*.inoreader.com/recent*
  17. // @match https://*.inoreader.com/search*
  18. // @match https://*.inoreader.com/channel*
  19. // @match https://*.inoreader.com/teams*
  20. // @match https://*.inoreader.com/dashboard*
  21. // @match https://*.inoreader.com/pocket*
  22. // @match https://*.inoreader.com/liked*
  23. // @match https://*.inoreader.com/tags*
  24. // @icon https://inoreader.com/favicon.ico?v=8
  25. // @license MIT
  26. // ==/UserScript==
  27. // @ts-check
  28.  
  29. (function () {
  30. "use strict";
  31.  
  32. document.head.insertAdjacentHTML("beforeend", `
  33. <style>
  34. .tm_dynamic_height {
  35. height: auto !important;
  36. }
  37. .tm_remove_position_setting {
  38. position: unset !important;
  39. }
  40. </style>`);
  41.  
  42. const appConfig = {
  43. corsProxy: "https://corsproxy.io/?",
  44. };
  45.  
  46. const appState = {
  47. tmObserverArticleListLinked: false,
  48. };
  49.  
  50. // Select the node that will be observed for mutations
  51. const targetNode = document.body;
  52.  
  53. // Options for the observer (which mutations to observe)
  54. const mutationObserverGlobalConfig = {
  55. attributes: false,
  56. childList: true,
  57. subtree: true,
  58. };
  59.  
  60. const querySelectorPathArticleRoot =
  61. ".article_full_contents .article_content";
  62. const querySelectorArticleContentWrapper = ".article_tile_content_wraper";
  63. const querySelectorArticleFooter = ".article_tile_footer";
  64.  
  65. /**
  66. * Callback function to execute when mutations are observed
  67. * @param {MutationRecord[]} mutationsList - List of mutations observed
  68. * @param {MutationObserver} observer - The MutationObserver instance
  69. */
  70. const callback = function (mutationsList, observer) {
  71. for (let mutation of mutationsList) {
  72. if (mutation.type === "childList") {
  73. mutation.addedNodes.forEach(function (node) {
  74. if (node.nodeType === Node.ELEMENT_NODE) {
  75. stylizeCardsInList(node);
  76. }
  77. });
  78. }
  79. }
  80. };
  81.  
  82. /**
  83. *
  84. * @param {Node} node
  85. * @returns {void}
  86. */
  87. function stylizeCardsInList(node) {
  88.  
  89. /**
  90. * @type {MutationObserver | undefined}
  91. */
  92. let tmObserverArticleList;
  93. const readerPane = document.body.querySelector("#reader_pane");
  94. if (readerPane) {
  95. if (!appState.tmObserverArticleListLinked) {
  96. appState.tmObserverArticleListLinked = true;
  97.  
  98. /**
  99. * Callback function to execute when mutations are observed
  100. * @param {MutationRecord[]} mutationsList - List of mutations observed
  101. * @param {MutationObserver} observer - The MutationObserver instance
  102. */
  103. const callback = function (mutationsList, observer) {
  104. for (let mutation of mutationsList) {
  105. if (mutation.type === "childList") {
  106. mutation.addedNodes.forEach(function (node) {
  107. if (node.nodeType === Node.ELEMENT_NODE) {
  108. if (appState.tmObserverArticleListLinked) {
  109. setTimeout(() => {
  110. start(node);
  111. }, 3500);
  112. // the second attempt is needed because some images or videos are loaded after the first attempt
  113. setTimeout(() => {
  114. start(node);
  115. }, 10000);
  116. }
  117. }
  118. });
  119. }
  120. }
  121. };
  122.  
  123. // Options for the observer (which mutations to observe)
  124. const mutationObserverLocalConfig = {
  125. attributes: false,
  126. childList: true,
  127. subtree: false,
  128. };
  129.  
  130. // Create an observer instance linked to the callback function
  131. tmObserverArticleList = new MutationObserver(callback);
  132.  
  133. // Start observing the target node for configured mutations
  134. tmObserverArticleList.observe(
  135. readerPane,
  136. mutationObserverLocalConfig
  137. );
  138. }
  139. } else {
  140. appState.tmObserverArticleListLinked = false;
  141. tmObserverArticleList?.disconnect();
  142. }
  143.  
  144. /**
  145. *
  146. * @param {Node} node
  147. */
  148. function start(node) {
  149. /**
  150. * @type {Node & HTMLDivElement}
  151. */
  152. // @ts-ignore
  153. const element = node;
  154. if (
  155. element.hasChildNodes() &&
  156. element.id.includes("article_") &&
  157. element.classList.contains("ar") &&
  158. !element.classList.contains("tm_dynamic_height")
  159. ) {
  160. // @ts-ignore
  161. const cardWidth = element.clientWidth ?? element.offsetWidth ?? element.scrollWidth;
  162. // @ts-ignore
  163. const cardHeight = element.clientHeight ?? element.offsetHeight ?? element.scrollHeight;
  164.  
  165. // 1. Set card height dynamic
  166. setDynamicHeight(element);
  167.  
  168. // 2. Set cotnent wrapper height dynamic
  169. const articleContentWrapperElement = element.querySelector(
  170. querySelectorArticleContentWrapper
  171. );
  172. if (articleContentWrapperElement) {
  173. setDynamicHeight(articleContentWrapperElement);
  174. }
  175.  
  176. // 3. Remove position setting from article footer
  177. const articleFooter = element.querySelector(
  178. querySelectorArticleFooter
  179. );
  180. if (articleFooter) {
  181. removePositionSetting(articleFooter);
  182. }
  183.  
  184. // 4. Find image height
  185. /**
  186. * @type {HTMLDivElement | null}
  187. */
  188. const divImageElement = element.querySelector(
  189. "a[href] > .article_tile_picture[style*='background-image']"
  190. );
  191. if (!divImageElement) {
  192. return;
  193. }
  194. const imageUrl = getImageLink(divImageElement);
  195. if (!imageUrl) {
  196. return;
  197. }
  198. const dimensions = getImageDimensions(imageUrl);
  199.  
  200. // 5. Set image height (and - automatically - the card height)
  201. dimensions.then(([width, height]) => {
  202. if (height > 0) {
  203. const calculatedHeight = Math.round(
  204. (cardWidth / width) * height
  205. );
  206. const pictureOldHeight =
  207. (divImageElement.clientHeight ??
  208. divImageElement.offsetHeight ??
  209. divImageElement.scrollHeight) ||
  210. cardHeight;
  211. /**
  212. * @type {HTMLDivElement}
  213. */
  214. // @ts-ignore
  215. const div = divImageElement;
  216. if (calculatedHeight > pictureOldHeight) {
  217. div.style.height = `${calculatedHeight}px`;
  218. }
  219.  
  220. // 5.1. Set card class to `.tm_dynamic_height` to not process it again next time
  221. element.classList?.add("tm_dynamic_height");
  222. }
  223. });
  224. }
  225. }
  226.  
  227. /**
  228. *
  229. * @param {Element} element
  230. * @returns {void}
  231. */
  232. function setDynamicHeight(element) {
  233. element.classList?.add("tm_dynamic_height");
  234. }
  235.  
  236. /**
  237. *
  238. * @param {Element} element
  239. * @returns {void}
  240. */
  241. function removeDynamicHeight(element) {
  242. const div = element.querySelector("img");
  243. if (!div) {
  244. return;
  245. }
  246. div.classList?.remove("tm_dynamic_height");
  247. }
  248.  
  249. /**
  250. *
  251. * @param {Element} element
  252. * @returns {void}
  253. */
  254. function removePositionSetting(element) {
  255. element.classList?.add("tm_remove_position_setting");
  256. }
  257.  
  258. /**
  259. *
  260. * @param {Element} element
  261. * @returns {void}
  262. */
  263. function restorePositionSetting(element) {
  264. element.classList?.remove("tm_remove_position_setting");
  265. }
  266.  
  267. /**
  268. *
  269. * @param {HTMLDivElement} div
  270. * @returns {string | null}
  271. */
  272. function getImageLink(div) {
  273. const backgroundImageUrl = div?.style.backgroundImage;
  274. /**
  275. * @type {string | undefined}
  276. */
  277. let imageUrl;
  278. try {
  279. imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
  280. } catch (error) {
  281. imageUrl = backgroundImageUrl?.slice(5, -2);
  282. }
  283.  
  284. if (!imageUrl || imageUrl == "undefined") {
  285. return null;
  286. }
  287.  
  288. if (!imageUrl?.startsWith("http")) {
  289. console.error(
  290. `The image could not be parsed. Image URL: ${imageUrl}`
  291. );
  292. return null;
  293. }
  294. return imageUrl;
  295. }
  296.  
  297. /**
  298. *
  299. * @param {string} url
  300. * @returns {Promise<[number, number]>}
  301. */
  302. async function getImageDimensions(url) {
  303. const img = new Image();
  304. img.src = url;
  305. try {
  306. await img.decode();
  307. } catch (error) {
  308. return Promise.reject(error);
  309. }
  310. return Promise.resolve([img.width, img.height]);
  311. };
  312. }
  313.  
  314. // Create an observer instance linked to the callback function
  315. const tmObserverDynamicHeight = new MutationObserver(callback);
  316.  
  317. // Start observing the target node for configured mutations
  318. tmObserverDynamicHeight.observe(targetNode, mutationObserverGlobalConfig);
  319.  
  320. /**
  321. *
  322. * @param {Function | void} mainFunction
  323. * @param {number} delay
  324. * @returns
  325. */
  326. function debounce(mainFunction, delay) {
  327. // Declare a variable called 'timer' to store the timer ID
  328. /**
  329. * @type {number}
  330. */
  331. let timer;
  332.  
  333. // Return an anonymous function that takes in any number of arguments
  334. /**
  335. * @param {...any} args
  336. * @returns {void}
  337. */
  338. return function (...args) {
  339. // Clear the previous timer to prevent the execution of 'mainFunction'
  340. clearTimeout(timer);
  341.  
  342. // Set a new timer that will execute 'mainFunction' after the specified delay
  343. timer = setTimeout(() => {
  344. // @ts-ignore
  345. mainFunction(...args);
  346. }, delay);
  347. };
  348. }
  349. })();