🏠 Home 

Hitomi page scroller

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();
})();