Hitomi page scroller

You can scroll up and down to turn the page and adjust the "Fit" mode image area.

  1. // ==UserScript==
  2. // @name:ko Hitomi 페이지 스크롤러
  3. // @name Hitomi page scroller
  4. // @name:ru Hitomi прокрутка страниц
  5. // @name:ja Hitomiページスクローラー
  6. // @name:zh-TW Hitomi頁面滾動條
  7. // @name:zh-CN Hitomi页面滚动条
  8.  
  9. // @description:ko 위아래로 스크롤하여 페이지를 넘길 수 있으며, "Fit"모드 이미지 넓이를 조절 할 수 있습니다.
  10. // @description You can scroll up and down to turn the page and adjust the "Fit" mode image area.
  11. // @description:ru Вы можете прокручивать страницы вверх и вниз, регулируя ширину изображения в режиме "Fit".
  12. // @description:ja 上下にスクロールしてページをめくることができ、「Fit」モードイメージの広さを調節することができます。
  13. // @description:zh-TW 可上下滾動翻頁,並可調整"Fit"模式圖像寬度。
  14. // @description:zh-CN 可上下滚动翻页,并可调整"Fit"模式图像宽度。
  15.  
  16. // @namespace https://ndaesik.tistory.com/
  17. // @version 2024.12.06.00.34
  18. // @author ndaesik
  19. // @icon https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://hitomi.la
  20. // @match https://*.la/reader/*
  21.  
  22. // @grant GM_getValue
  23. // @grant GM_setValue
  24. // ==/UserScript==
  25.  
  26. (function() {
  27. 'use strict';
  28. const startPage = parseInt(window.location.hash.slice(1)) || 1;
  29. let currentPage = startPage;
  30. let isLoading = false;
  31. let loadQueue = 0;
  32. let upScrollCount = 0;
  33. let lastScrollTime = 0;
  34. let initialLoad = true;
  35. const baseUrl = window.location.href.split('#')[0];
  36. let paddingSize = GM_getValue('paddingSize', 20);
  37.  
  38. const style = document.createElement('style');
  39. style.textContent = `
  40. #comicImages {
  41. padding: 0 ${paddingSize}vw !important;
  42. width: 100vw !important;
  43. box-sizing: border-box !important;
  44. user-select: none !important;
  45. display: block !important;
  46. position: relative !important;
  47. }
  48. #comicImages picture {
  49. pointer-events: none !important;
  50. display: block !important;
  51. padding: 3px 0 !important
  52. }
  53. #comicImages img {
  54. width: 100% !important;
  55. display: block !important;
  56. }
  57. #comicImages.fitVertical img {
  58. max-height: unset !important;
  59. }
  60. .width-control-container {
  61. display: flex !important;
  62. align-items: center !important;
  63. gap: 10px !important;
  64. padding: 12px !important;
  65. }
  66. .width-range {
  67. width: 100px !important;
  68. }
  69. #comicImages picture:first-child {
  70. min-height: 100vh !important;
  71. }
  72. `;
  73. document.head.appendChild(style);
  74.  
  75. function createPaddingControl() {
  76. const navbarNav = document.querySelector('.navbar-nav');
  77. if (!navbarNav) return;
  78.  
  79. const container = document.createElement('div');
  80. container.className = 'width-control-container';
  81. const range = document.createElement('input');
  82. range.type = 'range';
  83. range.className = 'width-range';
  84. range.min = '0';
  85. range.max = '45';
  86. range.value = 45 - paddingSize;
  87.  
  88. range.addEventListener('input', (e) => {
  89. paddingSize = 45 - e.target.value;
  90. document.querySelector('#comicImages').style.cssText += `padding: 0 ${paddingSize}vw !important;`;
  91. GM_setValue('paddingSize', paddingSize);
  92. });
  93.  
  94. container.appendChild(range);
  95. navbarNav.appendChild(container);
  96. }
  97.  
  98. function updateCurrentPage() {
  99. const container = document.querySelector('#comicImages');
  100. if (!container) return;
  101. const pictures = container.querySelectorAll('picture');
  102. if (!pictures.length) return;
  103.  
  104. const pageSelect = document.querySelector('#single-page-select');
  105. if (!pageSelect) return;
  106.  
  107. // 현재 뷰포트의 중앙 위치 계산
  108. const viewportHeight = window.innerHeight;
  109. const viewportCenter = window.scrollY + (viewportHeight / 2);
  110.  
  111. // 각 picture 요소의 위치 확인
  112. let closestPicture = null;
  113. let closestDistance = Infinity;
  114.  
  115. pictures.forEach((picture, index) => {
  116. const rect = picture.getBoundingClientRect();
  117. // picture 요소의 절대 위치 계산
  118. const pictureTop = rect.top + window.scrollY;
  119. const pictureCenter = pictureTop + (rect.height / 2);
  120. const distance = Math.abs(viewportCenter - pictureCenter);
  121.  
  122. if (distance < closestDistance) {
  123. closestDistance = distance;
  124. closestPicture = index;
  125. }
  126. });
  127.  
  128. // URL의 해시값을 기준으로 현재 페이지 계산
  129. if (closestPicture !== null) {
  130. const totalValue = parseInt(window.location.hash.slice(1)) + closestPicture;
  131. if (totalValue !== parseInt(pageSelect.value)) {
  132. pageSelect.value = totalValue;
  133. console.log(`Hash: ${window.location.hash}, Index: ${closestPicture}, Total: ${totalValue}`);
  134. }
  135. }
  136. }
  137.  
  138. function handleScrollWheel(e) {
  139. const container = document.querySelector('#comicImages');
  140. if (!container) return;
  141.  
  142. if (container.scrollTop === 0 && e.deltaY < 0) {
  143. const currentTime = Date.now();
  144. if (currentTime - lastScrollTime < 500) {
  145. upScrollCount++;
  146. if (upScrollCount >= 2) {
  147. const prevPanel = document.querySelector('#prevPanel');
  148. if (prevPanel) prevPanel.click();
  149. upScrollCount = 0;
  150. }
  151. } else {
  152. upScrollCount = 1;
  153. }
  154. lastScrollTime = currentTime;
  155. } else {
  156. upScrollCount = 0;
  157. }
  158. }
  159.  
  160. function initScrollListener() {
  161. const container = document.querySelector('#comicImages');
  162. if (!container) {
  163. setTimeout(initScrollListener, 100);
  164. return;
  165. }
  166.  
  167. let scrollTimeout;
  168. container.addEventListener('scroll', () => {
  169. if (scrollTimeout) return;
  170. scrollTimeout = setTimeout(() => {
  171. checkScrollAndLoad();
  172. updateCurrentPage();
  173. scrollTimeout = null;
  174. }, 50);
  175. });
  176.  
  177. container.addEventListener('wheel', handleScrollWheel);
  178. container.style.cssText += `padding: 0 ${paddingSize}vw !important;`;
  179.  
  180. document.querySelector('#single-page-select').value = startPage;
  181.  
  182. if (initialLoad) {
  183. loadNextImage();
  184. initialLoad = false;
  185. }
  186.  
  187. checkScrollAndLoad();
  188. updateCurrentPage();
  189. }
  190.  
  191. function getMaxPage() {
  192. const options = document.querySelectorAll('#single-page-select option');
  193. let maxPage = 0;
  194. options.forEach(option => {
  195. const value = parseInt(option.value);
  196. if (value > maxPage) maxPage = value;
  197. });
  198. return maxPage;
  199. }
  200.  
  201. async function loadNextImage() {
  202. if (isLoading) {
  203. loadQueue++;
  204. return;
  205. }
  206.  
  207. const maxPage = getMaxPage();
  208. if (currentPage >= maxPage) {
  209. loadQueue = 0;
  210. return;
  211. }
  212.  
  213. isLoading = true;
  214.  
  215. try {
  216. currentPage++;
  217. const iframe = document.createElement('iframe');
  218. iframe.style.display = 'none';
  219. document.body.appendChild(iframe);
  220. iframe.src = `${baseUrl}#${currentPage}`;
  221.  
  222. await new Promise(resolve => iframe.onload = resolve);
  223. const imgElement = await waitForElement(iframe, '#comicImages > picture > img');
  224.  
  225. if (!imgElement?.src) throw new Error('Image not found');
  226.  
  227. const pictureElement = document.createElement('picture');
  228. const newImage = document.createElement('img');
  229. newImage.src = imgElement.src;
  230. newImage.style.cssText = 'width: 100% !important; display: block !important;';
  231.  
  232. await new Promise((resolve, reject) => {
  233. newImage.onload = resolve;
  234. newImage.onerror = reject;
  235. });
  236.  
  237. pictureElement.appendChild(newImage);
  238. const container = document.querySelector('#comicImages');
  239. if (!container) throw new Error('Container not found');
  240.  
  241. container.appendChild(pictureElement);
  242. iframe.remove();
  243.  
  244. if (loadQueue > 0) {
  245. loadQueue--;
  246. loadNextImage();
  247. }
  248. checkScrollAndLoad();
  249. updateCurrentPage();
  250. } catch (error) {
  251. currentPage--;
  252. loadQueue = 0;
  253. } finally {
  254. isLoading = false;
  255. }
  256. }
  257.  
  258. function checkScrollAndLoad() {
  259. const container = document.querySelector('#comicImages');
  260. if (!container) return;
  261. const scrollPosition = container.scrollTop + container.clientHeight;
  262. const remainingHeight = container.scrollHeight - scrollPosition;
  263. if (remainingHeight < container.clientHeight * 2.5) loadNextImage();
  264. }
  265.  
  266. function waitForElement(iframe, selector, timeout = 5000) {
  267. return new Promise((resolve, reject) => {
  268. const startTime = Date.now();
  269. const check = () => {
  270. const element = iframe.contentDocument.querySelector(selector);
  271. if (element) return resolve(element);
  272. if (Date.now() - startTime > timeout) return reject(new Error(`Timeout`));
  273. setTimeout(check, 100);
  274. };
  275. check();
  276. });
  277. }
  278.  
  279. createPaddingControl();
  280. initScrollListener();
  281. })();