InoReader dynamic height of tiles in the card view

Makes cards' heights to be dynamic depending on image height

当前为 2024-10-24 提交的版本,查看 最新版本

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