Bible.com iframe specific styles

Adds a class to html if bible.com is in an iframe. Adjusts font size of parallel versions to fit the left column. Accepts parent scrolling messages.

当前为 2025-05-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Bible.com iframe specific styles
  3. // @namespace Violentmonkey Scripts
  4. // @match *://www.bible.com/*
  5. // @grant none
  6. // @version 1.8
  7. // 改善字體大細个調整成功率
  8. // @author Aiuanyu x Gemini
  9. // @description Adds a class to html if bible.com is in an iframe. Adjusts font size of parallel versions to fit the left column. Accepts parent scrolling messages.
  10. // @description:zh-TW 當 bible.com 在 iframe 裡時,給 <html> 加個 class。調整並列版本个字體大小,讓佇左邊个欄位內看起來較好。接受上層網頁共下捲動个命令。
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. if (window.self !== window.top) {
  17. document.documentElement.classList.add('is-in-iframe');
  18. console.log('Bible.com is in an iframe, added class "is-in-iframe" to <html>');
  19.  
  20. // 當 iframe 內容載入完成後,嘗試調整並列版本个字體大小
  21. window.addEventListener('load', function () {
  22. // 等待所有字體載入完成 (e.g., web fonts used by bible.com itself)
  23. document.fonts.ready.then(function () {
  24. // Add a small delay AFTER fonts are ready and page is loaded,
  25. // to give bible.com's own scripts more time to render dynamic content
  26. // before we start measuring heights.
  27. const initialAdjustmentDelay = 1500; // 1.5 秒鐘
  28. console.log(`All fonts loaded. Waiting ${initialAdjustmentDelay}ms before attempting font adjustment.`);
  29. setTimeout(function() {
  30. adjustParallelFontSize();
  31. }, initialAdjustmentDelay);
  32. }).catch(function (error) {
  33. console.warn('Font loading error or timeout, proceeding with font adjustment anyway:', error);
  34. setTimeout(function() { adjustParallelFontSize(); }, 1500); // 若有錯誤,也延遲一息仔再試
  35. });
  36. });
  37. // 監聽來自父視窗 (index.html) 的訊息
  38. window.addEventListener('message', function(event) {
  39. // 為著安全,可以檢查訊息來源 event.origin
  40. // 但因為 index.html 可能係 file:// 協定,event.origin 會係 'null'
  41. // 所以,檢查 event.source 是不是 window.top 會較穩當
  42. if (event.source !== window.top) {
  43. // console.log('Userscript: Message ignored, not from top window.');
  44. return;
  45. }
  46.  
  47. if (event.data && event.data.type === 'SYNC_SCROLL_TO_PERCENTAGE') {
  48. const percentage = parseFloat(event.data.percentage);
  49. if (isNaN(percentage) || percentage < 0 || percentage > 1) {
  50. console.warn('Userscript: Invalid scroll percentage received:', event.data.percentage);
  51. return;
  52. }
  53.  
  54. const de = document.documentElement;
  55. const scrollableDistance = de.scrollHeight - de.clientHeight;
  56.  
  57. if (scrollableDistance <= 0) {
  58. // console.log('Userscript: Content is not scrollable.');
  59. return; // 內容毋使捲動
  60. }
  61.  
  62. const scrollToY = scrollableDistance * percentage;
  63. // console.log(`Userscript: Scrolling to ${percentage*100}%, ${scrollToY}px. Scrollable: ${scrollableDistance}, Total: ${de.scrollHeight}, Visible: ${de.clientHeight}`);
  64. window.scrollTo({ top: scrollToY, behavior: 'auto' }); // 'auto' 表示立即捲動
  65. }
  66. });
  67.  
  68. } else {
  69. console.log('Bible.com is top level window.');
  70. }
  71. function adjustParallelFontSize(retryAttempt = 0) {
  72. const MAX_RETRIES = 10; // 增加重試次數
  73. const RETRY_DELAY = 1200; // 每次重試之間等 1.2 秒鐘
  74. const MIN_COLUMN_HEIGHT_THRESHOLD = 50; // px, 用來判斷內容敢有顯示出來个基本高度
  75.  
  76. try { // try...catch 包住整個函數个內容
  77. const params = new URLSearchParams(window.location.search);
  78. const parallelVersionId = params.get('parallel');
  79.  
  80. if (!parallelVersionId) {
  81. console.log('No parallel version ID found in URL, skipping font adjustment.');
  82. return;
  83. }
  84.  
  85. // 取得左欄主要版本个 ID
  86. const pathSegments = window.location.pathname.match(/\/bible\/(\d+)\//);
  87. if (!pathSegments || !pathSegments[1]) {
  88. console.log('Could not extract main version ID from path, skipping font adjustment.');
  89. return;
  90. }
  91. const mainVersionId = pathSegments[1];
  92.  
  93. const leftDataVidSelector = `[data-vid="${mainVersionId}"]`;
  94. const rightDataVidSelector = `[data-vid="${parallelVersionId}"]`;
  95.  
  96. // 找出並列閱讀个主要容器 (根據先前个 HTML 結構)
  97. const parallelContainer = document.querySelector('div.grid.md\\:grid-cols-2, div.grid.grid-cols-1.md\\:grid-cols-2');
  98. if (!parallelContainer) {
  99. console.log('Parallel container (e.g., div.grid.md:grid-cols-2) not found.');
  100. return;
  101. }
  102.  
  103. const columns = Array.from(parallelContainer.children).filter(el => getComputedStyle(el).display !== 'none');
  104. if (columns.length < 2) {
  105. console.log('Less than two visible columns found in parallel container.');
  106. return;
  107. }
  108.  
  109. const leftColumnEl = columns[0];
  110. const rightColumnEl = columns[1];
  111.  
  112. const leftVersionDiv = leftColumnEl.querySelector(leftDataVidSelector);
  113. const rightVersionDiv = rightColumnEl.querySelector(rightDataVidSelector);
  114.  
  115. if (!leftVersionDiv || !rightVersionDiv) {
  116. console.log(`Content div for left (${leftDataVidSelector}) or right (${rightDataVidSelector}) not found.`);
  117. if (retryAttempt < MAX_RETRIES -1) { // 為元素搜尋保留一些重試次數
  118. console.warn(`Retrying element search in ${RETRY_DELAY}ms (Attempt ${retryAttempt + 1}/${MAX_RETRIES})`);
  119. setTimeout(() => adjustParallelFontSize(retryAttempt + 1), RETRY_DELAY);
  120. } else {
  121. console.error('Element search failed after max retries for content divs. Aborting font adjustment.');
  122. }
  123. return; // 若元素無尋到,愛 return 避免錯誤
  124. }
  125.  
  126. // At this point, elements are found. Now check if they have rendered content.
  127. // Force reflow before measurement
  128. leftVersionDiv.offsetHeight;
  129. rightVersionDiv.offsetHeight; // Ensure reflow before measurement
  130. const currentLeftHeight = leftVersionDiv.offsetHeight;
  131.  
  132. if (currentLeftHeight < MIN_COLUMN_HEIGHT_THRESHOLD && retryAttempt < MAX_RETRIES) { // 若左欄高度無夠
  133. console.warn(`Left column height (${currentLeftHeight}px) is less than threshold (${MIN_COLUMN_HEIGHT_THRESHOLD}px). Content might not be fully rendered. Retrying in ${RETRY_DELAY}ms (Attempt ${retryAttempt + 1}/${MAX_RETRIES})`);
  134. setTimeout(() => adjustParallelFontSize(retryAttempt + 1), RETRY_DELAY);
  135. return;
  136. }
  137. if (currentLeftHeight < MIN_COLUMN_HEIGHT_THRESHOLD && retryAttempt >= MAX_RETRIES) { // 重試了後還係無夠高
  138. console.error(`Left column height (${currentLeftHeight}px) remains below threshold after ${MAX_RETRIES} retries. Aborting font adjustment.`);
  139. return; // 放棄調整
  140. }
  141.  
  142. console.log('Found leftVersionDiv:', leftVersionDiv, 'Found rightVersionDiv:', rightVersionDiv);
  143.  
  144. // 使用 requestAnimationFrame 來確保 DOM 操作和測量是在瀏覽器準備好繪製下一幀之前進行
  145. // If we reach here, elements are found and left column has some content.
  146. requestAnimationFrame(() => {
  147. // 開始調整字體
  148. let currentFontSize = 90; // 初始字體大小
  149. rightVersionDiv.style.fontSize = currentFontSize + '%';
  150.  
  151. // The leftHeight from *before* rAF (currentLeftHeight) should be the reference.
  152. let leftHeight = currentLeftHeight;
  153. // Ensure right column also reflows with its new font size
  154. let rightHeight = rightVersionDiv.offsetHeight; // 獲取初始高度
  155.  
  156. console.log(`Initial check at ${currentFontSize}%: Right height ${rightHeight}px, Left height ${leftHeight}px (reference)`);
  157.  
  158. // 如果初始字體大小就已經讓右邊內容不比左邊長,就不用調整了
  159. if (rightHeight <= leftHeight) {
  160. console.log('Initial font size ' + currentFontSize + '% is sufficient or shorter.');
  161. return;
  162. }
  163.  
  164. // 如果初始字體大小讓右邊內容比左邊長,就開始縮小字體
  165. // 預設使用最小个測試字體 (50%),假使所有測試過个字體都還係分右邊太長。
  166. let bestFitFontSize = 50;
  167. let foundOptimalAdjustment = false;
  168.  
  169. for (let testSize = currentFontSize - 1; testSize >= 50; testSize--) { // 從比初始值小1%開始,最細到 50%
  170. rightVersionDiv.style.fontSize = testSize + '%';
  171. rightHeight = rightVersionDiv.offsetHeight; // 每次改變字體大小後,重新獲取高度 (強制 reflow)
  172.  
  173. if (rightHeight > leftHeight) {
  174. // 這隻 testSize 還係分右邊太長,繼續試較細个字體。
  175. // 假使這係迴圈最後一次 (testSize == 50) 而且還係太長,
  176. // bestFitFontSize 會維持在 50%。
  177. } else {
  178. // 這隻 testSize 分右邊內容變到毋比左邊長了 (<=)。
  179. // 照你个要求,𠊎等愛用前一隻字體大細 (testSize + 1),
  180. // 因為該隻字體大細會分右邊「略略仔長過左邊」。
  181. bestFitFontSize = testSize + 1;
  182. foundOptimalAdjustment = true;
  183. console.log(`Right content became shorter/equal at ${testSize}% (height ${rightHeight}px). Applying previous size ${bestFitFontSize}% which was slightly taller.`);
  184. break; // 尋到臨界點了,跳出迴圈
  185. }
  186. }
  187.  
  188. if (!foundOptimalAdjustment && currentFontSize > 50) {
  189. // 假使迴圈跑完,foundOptimalAdjustment 還係 false,
  190. // 表示從 (currentFontSize - 1) 到 50% 所有字體都還係分右邊太長。
  191. // 在這情況下,bestFitFontSize 已經係 50%。
  192. console.log(`All tested font sizes from ${currentFontSize - 1}% down to 50% still made right content taller. Using smallest tested size: 50%.`);
  193. }
  194.  
  195. // 迴圈結束後,將字體設定為決定好个大小
  196. rightVersionDiv.style.fontSize = bestFitFontSize + '%';
  197. // 為著準確記錄最終狀態,重新量一次高度
  198. const finalRightHeight = rightVersionDiv.offsetHeight;
  199. console.log('Final adjusted font size for ' + rightDataVidSelector + ' is ' + bestFitFontSize + '%. Final Right content: ' + finalRightHeight + 'px, Left content: ' + leftHeight + 'px');
  200. });
  201. } catch (error) {
  202. console.error('Error during font adjustment:', error);
  203. }
  204. }
  205. })();