InoReader dynamic height of tiles in the card view

Makes cards' heights to be dynamic depending on image height

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

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