Converts gallery preview into webtoon-style vertical scroll using Hitomi's own URL functions.
// ==UserScript==
// @name Hitomi Webtoon Reader
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Converts gallery preview into webtoon-style vertical scroll using Hitomi's own URL functions.
// @match *://hitomi.la/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Only run on gallery pages
const match = window.location.pathname.match(/-?([0-9]+)(?:\.html)?$/);
if (!match || !match[1]) return;
const galleryid = parseInt(match[1]);
let done = false;
const timer = setInterval(() => {
if (done) return;
// Wait for Hitomi's own functions AND gallery data to be ready
if (typeof galleryinfo === 'undefined' || !galleryinfo) return;
if (typeof url_from_url_from_hash !== 'function') return;
if (typeof gg === 'undefined' || typeof gg.m !== 'function') return;
const files = galleryinfo.files;
if (!files || files.length === 0) return;
done = true;
clearInterval(timer);
// Build webtoon container
const webtoonBox = document.createElement('div');
webtoonBox.id = 'webtoon-reader';
webtoonBox.style.cssText = 'display:flex;flex-direction:column;width:100%;background:#111;margin:0 auto;padding:0;';
files.forEach((image) => {
const newImg = document.createElement('img');
// Use Hitomi's own function to generate the correct full-res URL
// This is exactly what the reader page does internally
let src;
try {
if (image.hasavif) {
src = url_from_url_from_hash(galleryid, image, 'avif', 'avif');
} else if (image.haswebp) {
src = url_from_url_from_hash(galleryid, image, 'webp', 'webp');
} else {
const ext = image.name.split('.').pop();
src = url_from_url_from_hash(galleryid, image, 'images', ext);
}
} catch(e) {
// Fallback: try without format preference
try {
const ext = image.name.split('.').pop();
src = url_from_url_from_hash(galleryid, image, 'images', ext);
} catch(e2) {
console.error('Failed to generate URL for', image.name, e2);
return;
}
}
if (!src) return;
newImg.src = src;
newImg.loading = 'lazy';
newImg.style.cssText = 'width:100%;max-width:100vw;height:auto;display:block;margin:0 auto;border:none;min-height:300px;background:#222;';
newImg.onload = function() { this.style.minHeight = 'auto'; };
webtoonBox.appendChild(newImg);
});
// Hide original thumbnail area and pagination
const previewContainer = document.querySelector('.gallery-preview');
if (previewContainer) {
previewContainer.classList.remove('lillie');
// Hide old thumbnails
const thumbList = previewContainer.querySelector('.thumbnail-list');
if (thumbList) thumbList.style.display = 'none';
const pagerNav = previewContainer.querySelector('.simplePagerNav');
if (pagerNav) pagerNav.style.display = 'none';
// Insert webtoon reader
previewContainer.appendChild(webtoonBox);
previewContainer.style.padding = '0';
}
// Also hide related galleries section to keep focus on reading
document.querySelectorAll('.simplePagerNav').forEach(el => el.style.display = 'none');
}, 300);
})();