InoReader dynamic height of tiles in the card view

Makes cards' heights to be dynamic depending on image height

  1. // ==UserScript==
  2. // @name InoReader dynamic height of tiles in the card view
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.0
  5. // @description Makes cards' heights to be dynamic depending on image height
  6. // @author Kenya-West
  7. // @match https://*.inoreader.com/*
  8. // @icon https://inoreader.com/favicon.ico?v=8
  9. // @grant GM_addStyle
  10. // @license MIT
  11. // ==/UserScript==
  12. // @ts-check
  13.  
  14.  
  15. (async function () {
  16. const arriveEventConfigArticle = {
  17. fireOnAttributesModification: false,
  18. existing: true
  19. };
  20. const style = `
  21. .tm_dynamic_height {
  22. height: auto !important;
  23. }
  24. .tm_remove_position_setting {
  25. position: unset !important;
  26. }
  27. `;
  28.  
  29. // @ts-ignore
  30. GM_addStyle(style);
  31.  
  32. // @ts-ignore
  33. const arrive = await import("https://cdnjs.cloudflare.com/ajax/libs/arrive/2.5.2/arrive.min.js");
  34.  
  35. // @ts-ignore
  36. document.querySelector("#reader_pane")?.arrive(".ar", arriveEventConfigArticle, (article) => start(article));
  37.  
  38. const querySelectorPathArticleRoot =
  39. ".article_full_contents .article_content";
  40. const querySelectorArticleContentWrapper = ".article_tile_content_wraper";
  41. const querySelectorArticleFooter = ".article_tile_footer";
  42.  
  43.  
  44. function start(element) {
  45.  
  46. if (notHaveDynamicHeight(element)) {
  47. // @ts-ignore
  48. const cardWidth = element.clientWidth ?? element.offsetWidth ?? element.scrollWidth;
  49. // @ts-ignore
  50. const cardHeight = element.clientHeight ?? element.offsetHeight ?? element.scrollHeight;
  51.  
  52. // 1. Set card height dynamic
  53. setDynamicHeight(element);
  54.  
  55. // 2. Set content wrapper height dynamic
  56. const articleContentWrapperElement = element.querySelector(
  57. querySelectorArticleContentWrapper
  58. );
  59. if (articleContentWrapperElement) {
  60. setDynamicHeight(articleContentWrapperElement);
  61. }
  62.  
  63. // 3. Remove position setting from article footer
  64. const articleFooter = element.querySelector(
  65. querySelectorArticleFooter
  66. );
  67. if (articleFooter) {
  68. removePositionSetting(articleFooter);
  69. }
  70.  
  71. // 4. Find image height
  72. /**
  73. * @type {HTMLDivElement | null}
  74. */
  75. const divImageElement = element.querySelector(
  76. "a[href] > .article_tile_picture[style*='background-image']"
  77. );
  78. if (!divImageElement) {
  79. return;
  80. }
  81. const imageUrl = getImageLink(divImageElement);
  82. if (!imageUrl) {
  83. return;
  84. }
  85. const dimensions = getImageDimensions(imageUrl);
  86.  
  87. // 5. Set image height (and - automatically - the card height)
  88. dimensions.then(([width, height]) => {
  89. if (height > 0) {
  90. const calculatedHeight = Math.round(
  91. (cardWidth / width) * height
  92. );
  93. const pictureOldHeight =
  94. (divImageElement.clientHeight ??
  95. divImageElement.offsetHeight ??
  96. divImageElement.scrollHeight) ||
  97. cardHeight;
  98. /**
  99. * @type {HTMLDivElement}
  100. */
  101. const div = divImageElement;
  102. if (calculatedHeight > pictureOldHeight) {
  103. div.style.height = `${calculatedHeight}px`;
  104. }
  105.  
  106. // 5.1. Set card class to `.tm_dynamic_height` to not process it again next time
  107. element.classList?.add("tm_dynamic_height");
  108. }
  109. });
  110. }
  111. }
  112.  
  113. /**
  114. * Checks if article already has dynamic height set.
  115. */
  116. function notHaveDynamicHeight(element) {
  117. return element?.hasChildNodes() &&
  118. element?.id?.includes("article_") &&
  119. element?.classList.contains("ar") &&
  120. !element?.classList.contains("tm_dynamic_height");
  121. }
  122.  
  123. /**
  124. *
  125. * @param {Element} element
  126. * @returns {void}
  127. */
  128. function setDynamicHeight(element) {
  129. element.classList?.add("tm_dynamic_height");
  130. }
  131.  
  132. /**
  133. *
  134. * @param {Element} element
  135. * @returns {void}
  136. */
  137. function removeDynamicHeight(element) {
  138. const div = element.querySelector("img");
  139. if (!div) {
  140. return;
  141. }
  142. div.classList?.remove("tm_dynamic_height");
  143. }
  144.  
  145. /**
  146. *
  147. * @param {Element} element
  148. * @returns {void}
  149. */
  150. function removePositionSetting(element) {
  151. element.classList?.add("tm_remove_position_setting");
  152. }
  153.  
  154. /**
  155. *
  156. * @param {Element} element
  157. * @returns {void}
  158. */
  159. function restorePositionSetting(element) {
  160. element.classList?.remove("tm_remove_position_setting");
  161. }
  162.  
  163.  
  164. /**
  165. *
  166. * @param {HTMLDivElement} div
  167. * @returns {string | null}
  168. */
  169. function getImageLink(div) {
  170. const backgroundImageUrl = div?.style.backgroundImage;
  171. /**
  172. * @type {string | undefined}
  173. */
  174. let imageUrl;
  175. try {
  176. imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
  177. } catch (error) {
  178. imageUrl = backgroundImageUrl?.slice(5, -2);
  179. }
  180.  
  181. if (!imageUrl || imageUrl == "undefined") {
  182. return null;
  183. }
  184.  
  185. if (!imageUrl?.startsWith("http")) {
  186. console.error(
  187. `The image could not be parsed. Image URL: ${imageUrl}`
  188. );
  189. return null;
  190. }
  191. return imageUrl;
  192. }
  193.  
  194. /**
  195. *
  196. * @param {string} url
  197. * @returns {Promise<[number, number]>}
  198. */
  199. async function getImageDimensions(url) {
  200. const img = new Image();
  201. img.src = url;
  202. try {
  203. await img.decode();
  204. } catch (error) {
  205. return Promise.reject(error);
  206. }
  207. return Promise.resolve([img.width, img.height]);
  208. };
  209. })();
  210.