You can scroll up and down to turn the page and adjust the "Fit" mode image area.
// ==UserScript== // @name:ko Hitomi 페이지 스크롤러 // @name Hitomi page scroller // @name:ru Hitomi прокрутка страниц // @name:ja Hitomiページスクローラー // @name:zh-TW Hitomi頁面滾動條 // @name:zh-CN Hitomi页面滚动条 // @description:ko 위아래로 스크롤하여 페이지를 넘길 수 있으며, "Fit"모드 이미지 넓이를 조절 할 수 있습니다. // @description You can scroll up and down to turn the page and adjust the "Fit" mode image area. // @description:ru Вы можете прокручивать страницы вверх и вниз, регулируя ширину изображения в режиме "Fit". // @description:ja 上下にスクロールしてページをめくることができ、「Fit」モードイメージの広さを調節することができます。 // @description:zh-TW 可上下滾動翻頁,並可調整"Fit"模式圖像寬度。 // @description:zh-CN 可上下滚动翻页,并可调整"Fit"模式图像宽度。 // @namespace https://ndaesik.tistory.com/ // @version 2024.12.06.00.34 // @author ndaesik // @icon https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://hitomi.la // @match https://*.la/reader/* // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function() { 'use strict'; const startPage = parseInt(window.location.hash.slice(1)) || 1; let currentPage = startPage; let isLoading = false; let loadQueue = 0; let upScrollCount = 0; let lastScrollTime = 0; let initialLoad = true; const baseUrl = window.location.href.split('#')[0]; let paddingSize = GM_getValue('paddingSize', 20); const style = document.createElement('style'); style.textContent = ` #comicImages { padding: 0 ${paddingSize}vw !important; width: 100vw !important; box-sizing: border-box !important; user-select: none !important; display: block !important; position: relative !important; } #comicImages picture { pointer-events: none !important; display: block !important; padding: 3px 0 !important } #comicImages img { width: 100% !important; display: block !important; } #comicImages.fitVertical img { max-height: unset !important; } .width-control-container { display: flex !important; align-items: center !important; gap: 10px !important; padding: 12px !important; } .width-range { width: 100px !important; } #comicImages picture:first-child { min-height: 100vh !important; } `; document.head.appendChild(style); function createPaddingControl() { const navbarNav = document.querySelector('.navbar-nav'); if (!navbarNav) return; const container = document.createElement('div'); container.className = 'width-control-container'; const range = document.createElement('input'); range.type = 'range'; range.className = 'width-range'; range.min = '0'; range.max = '45'; range.value = 45 - paddingSize; range.addEventListener('input', (e) => { paddingSize = 45 - e.target.value; document.querySelector('#comicImages').style.cssText += `padding: 0 ${paddingSize}vw !important;`; GM_setValue('paddingSize', paddingSize); }); container.appendChild(range); navbarNav.appendChild(container); } function updateCurrentPage() { const container = document.querySelector('#comicImages'); if (!container) return; const pictures = container.querySelectorAll('picture'); if (!pictures.length) return; const pageSelect = document.querySelector('#single-page-select'); if (!pageSelect) return; // 현재 뷰포트의 중앙 위치 계산 const viewportHeight = window.innerHeight; const viewportCenter = window.scrollY + (viewportHeight / 2); // 각 picture 요소의 위치 확인 let closestPicture = null; let closestDistance = Infinity; pictures.forEach((picture, index) => { const rect = picture.getBoundingClientRect(); // picture 요소의 절대 위치 계산 const pictureTop = rect.top + window.scrollY; const pictureCenter = pictureTop + (rect.height / 2); const distance = Math.abs(viewportCenter - pictureCenter); if (distance < closestDistance) { closestDistance = distance; closestPicture = index; } }); // URL의 해시값을 기준으로 현재 페이지 계산 if (closestPicture !== null) { const totalValue = parseInt(window.location.hash.slice(1)) + closestPicture; if (totalValue !== parseInt(pageSelect.value)) { pageSelect.value = totalValue; console.log(`Hash: ${window.location.hash}, Index: ${closestPicture}, Total: ${totalValue}`); } } } function handleScrollWheel(e) { const container = document.querySelector('#comicImages'); if (!container) return; if (container.scrollTop === 0 && e.deltaY < 0) { const currentTime = Date.now(); if (currentTime - lastScrollTime < 500) { upScrollCount++; if (upScrollCount >= 2) { const prevPanel = document.querySelector('#prevPanel'); if (prevPanel) prevPanel.click(); upScrollCount = 0; } } else { upScrollCount = 1; } lastScrollTime = currentTime; } else { upScrollCount = 0; } } function initScrollListener() { const container = document.querySelector('#comicImages'); if (!container) { setTimeout(initScrollListener, 100); return; } let scrollTimeout; container.addEventListener('scroll', () => { if (scrollTimeout) return; scrollTimeout = setTimeout(() => { checkScrollAndLoad(); updateCurrentPage(); scrollTimeout = null; }, 50); }); container.addEventListener('wheel', handleScrollWheel); container.style.cssText += `padding: 0 ${paddingSize}vw !important;`; document.querySelector('#single-page-select').value = startPage; if (initialLoad) { loadNextImage(); initialLoad = false; } checkScrollAndLoad(); updateCurrentPage(); } function getMaxPage() { const options = document.querySelectorAll('#single-page-select option'); let maxPage = 0; options.forEach(option => { const value = parseInt(option.value); if (value > maxPage) maxPage = value; }); return maxPage; } async function loadNextImage() { if (isLoading) { loadQueue++; return; } const maxPage = getMaxPage(); if (currentPage >= maxPage) { loadQueue = 0; return; } isLoading = true; try { currentPage++; const iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); iframe.src = `${baseUrl}#${currentPage}`; await new Promise(resolve => iframe.onload = resolve); const imgElement = await waitForElement(iframe, '#comicImages > picture > img'); if (!imgElement?.src) throw new Error('Image not found'); const pictureElement = document.createElement('picture'); const newImage = document.createElement('img'); newImage.src = imgElement.src; newImage.style.cssText = 'width: 100% !important; display: block !important;'; await new Promise((resolve, reject) => { newImage.onload = resolve; newImage.onerror = reject; }); pictureElement.appendChild(newImage); const container = document.querySelector('#comicImages'); if (!container) throw new Error('Container not found'); container.appendChild(pictureElement); iframe.remove(); if (loadQueue > 0) { loadQueue--; loadNextImage(); } checkScrollAndLoad(); updateCurrentPage(); } catch (error) { currentPage--; loadQueue = 0; } finally { isLoading = false; } } function checkScrollAndLoad() { const container = document.querySelector('#comicImages'); if (!container) return; const scrollPosition = container.scrollTop + container.clientHeight; const remainingHeight = container.scrollHeight - scrollPosition; if (remainingHeight < container.clientHeight * 2.5) loadNextImage(); } function waitForElement(iframe, selector, timeout = 5000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { const element = iframe.contentDocument.querySelector(selector); if (element) return resolve(element); if (Date.now() - startTime > timeout) return reject(new Error(`Timeout`)); setTimeout(check, 100); }; check(); }); } createPaddingControl(); initScrollListener(); })();