Sleazy Fork is available in English.
Continuous reading mode with floating page control and ultra-fast loading
// ==UserScript==
// @name e-hentai Plus
// @namespace http://tampermonkey.net/
// @version 2.2
// @author Viki
// @description Continuous reading mode with floating page control and ultra-fast loading
// @license MIT
// @homepageURL https://github.com/Leovikii/e-hentai-plus
// @match https://e-hentai.org/g/*
// @match https://exhentai.org/g/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setValue
// ==/UserScript==
(function () {
'use strict';
const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):(document.head||document.documentElement).appendChild(document.createElement("style")).append(t);})(e));};
importCSS(" *,:before,:after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / .5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / .5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: } ");
const stylesCss = "html,body{background-color:#111!important;color:#ccc!important;margin:0;overflow-x:hidden}#gdt{display:flex;flex-direction:column;align-items:center;width:100%;max-width:1200px;margin:auto;padding-bottom:100px}.page-batch{width:100%;display:flex;flex-direction:column;align-items:center;margin-bottom:60px}.r-img{display:block;width:auto;max-width:100%;margin-bottom:20px;background:transparent;box-shadow:0 0 20px #00000080}.r-ph{color:#555;margin-bottom:50px;text-align:center;min-height:400px;display:flex;align-items:center;justify-content:center;font-family:sans-serif;font-size:18px;border:1px dashed #333;width:100%;flex-direction:column;gap:10px}.r-ph.loading{color:#888;border-color:#555}.r-ph.error{color:#d44;border-color:#d44}.retry-btn{padding:8px 16px;background:#333;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:14px;margin-top:10px}.retry-btn:hover{background:#555}.float-control{position:fixed;right:30px;bottom:30px;z-index:9999;display:flex;flex-direction:row;align-items:center;gap:0;transition:opacity .3s}.float-control.hidden{opacity:0;pointer-events:none}.side-btn{width:36px;height:36px;background:#333;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .3s;opacity:0;pointer-events:none}.float-control:hover .side-btn{opacity:1;pointer-events:auto}.side-btn:hover{background:#555;transform:scale(1.1)}.side-btn svg{width:18px;height:18px;fill:#fff}.side-btn.active{background:#4caf50}.side-btn.active:hover{background:#5cbf60}.auto-play-btn.hidden{display:none}.circle-control{width:50px;height:50px;background:#1a1a1a;border:2px solid #555;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .3s;box-shadow:0 4px 12px #00000080;margin:0 6px}.circle-control:hover{border-color:#888;box-shadow:0 6px 16px #000000b3;transform:scale(1.05)}.circle-control svg{width:24px;height:24px;fill:#fff}.settings-btn{cursor:pointer}.settings-panel{position:absolute;bottom:calc(100% + 10px);right:0;background:#1a1a1a;border:1px solid #555;border-radius:8px;padding:12px;min-width:180px;opacity:0;pointer-events:none;transition:all .3s;box-shadow:0 4px 12px #00000080;transform:translateY(5px)}.settings-panel.show{opacity:1;pointer-events:auto;transform:translateY(0)}.settings-item{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;font-size:13px;color:#ccc}.settings-item:last-child{margin-bottom:0}.settings-label{margin-right:10px;white-space:nowrap}.toggle-switch{width:40px;height:20px;background:#333;border-radius:10px;position:relative;cursor:pointer;transition:background .3s}.toggle-switch.on{background:#4caf50}.toggle-slider{width:16px;height:16px;background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;transition:left .3s}.toggle-switch.on .toggle-slider{left:22px}.interval-input{width:60px;background:#333;border:1px solid #555;border-radius:4px;color:#fff;padding:4px 8px;font-size:12px;text-align:center}.interval-input:focus{outline:none;border-color:#888}.single-page-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#000;z-index:9998;display:none;align-items:center;justify-content:center}.single-page-overlay.active{display:flex}.sp-image-container{width:100%;height:100%;display:flex;align-items:center;justify-content:center;position:relative}.sp-current-image{width:100%;height:100%;object-fit:contain;-webkit-user-select:none;user-select:none}.sp-close-btn{position:absolute;top:20px;right:20px;width:40px;height:40px;background:#333c;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:24px;color:#fff;transition:all .3s;z-index:10}.sp-close-btn:hover{background:#555555e6;transform:scale(1.1)}.sp-scrollbar{position:absolute;right:40px;top:10%;width:12px;height:80%;background:#2828284d;border-radius:6px;z-index:10;transition:background .3s}.sp-scrollbar:hover{background:#32323280}.sp-scrollbar-thumb{position:absolute;left:0;width:100%;min-height:60px;background:#fff6;border-radius:6px;transition:background .3s;cursor:grab;-webkit-user-select:none;user-select:none}.sp-scrollbar-thumb:hover{background:#fff9}.sp-scrollbar-thumb:active{cursor:grabbing;background:#ffffffb3}.sp-scrollbar-label{position:absolute;right:calc(100% + 16px);top:50%;transform:translateY(-50%);background:#1a1a1af2;padding:8px 14px;border-radius:8px;color:#fff;font-family:monospace;font-size:14px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .3s;box-shadow:0 2px 8px #0000004d}.sp-scrollbar:hover .sp-scrollbar-label{opacity:1}.sp-loading{color:#888;font-size:18px;font-family:sans-serif}.sp-placeholder{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative}.sp-placeholder-pulse{width:60%;max-width:500px;height:60%;background:#181818;border-radius:8px;animation:sp-pulse 1.5s ease-in-out infinite}.sp-placeholder-text{position:absolute;color:#666;font-family:monospace;font-size:16px;-webkit-user-select:none;user-select:none}@keyframes sp-pulse{0%,to{opacity:.3}50%{opacity:.6}}";
importCSS(stylesCss);
var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
var _GM_registerMenuCommand = (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
const CFG = {
nextPage: "3000px 0px",
prefetchDistance: 5e3,
maxRetries: 3,
retryDelay: 1e3
};
function loadSettings() {
return {
autoScroll: _GM_getValue("autoScroll", true),
showControl: _GM_getValue("showControl", true),
autoEnterSinglePage: _GM_getValue("autoEnterSinglePage", false),
autoPlay: _GM_getValue("autoPlay", false),
autoPlayInterval: _GM_getValue("autoPlayInterval", 3e3)
};
}
class Store {
constructor() {
this.listeners = new Map();
this.currPage = 1;
this.totalPage = 1;
this.nextUrl = null;
this.isFetching = false;
this.nextPagePrefetched = false;
this.currentImageIndex = 0;
this.allImages = [];
this.autoPlayTimer = null;
this._settings = loadSettings();
}
get settings() {
return this._settings;
}
updateSetting(key, value) {
this._settings[key] = value;
_GM_setValue(key, value);
this.emit("settingsChanged");
}
on(event, listener) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(listener);
}
emit(event) {
var _a;
(_a = this.listeners.get(event)) == null ? void 0 : _a.forEach((fn) => fn());
}
}
const store = new Store();
const q = (selector, root = document) => root.querySelector(selector);
const qa = (selector, root = document) => root.querySelectorAll(selector);
const HIDDEN_SELECTORS = ["#nb", "#fb", "#cdiv", ".gt", ".gpc", ".ptt", "#db"];
function hideOriginalElements() {
HIDDEN_SELECTORS.forEach((sel) => {
const el = q(sel);
if (el) el.style.display = "none";
});
}
function calcTotal(doc, fallbackLinkCount) {
const gpc = q(".gpc", doc);
if (gpc) {
const txt = gpc.textContent ?? "";
const m = txt.match(/of\s+(\d+)\s+images/);
if (m && m[1]) {
const totalImgs = parseInt(m[1]);
const perPage = fallbackLinkCount || 20;
return Math.ceil(totalImgs / perPage);
}
}
const allLinks = Array.from(qa(".ptt td a", doc));
const lastA = allLinks.pop();
if (lastA) {
const t = parseInt(lastA.textContent ?? "");
if (!isNaN(t)) return t;
}
return 1;
}
function getNextUrl(doc) {
const ptt = q(".ptt", doc);
if (!ptt) return null;
const nextBtn = Array.from(qa("td a", ptt)).find((a) => (a.textContent ?? "").includes(">"));
return nextBtn ? nextBtn.href : null;
}
const parser = new DOMParser();
async function loadImageWithRetry(url, retries = 0) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const doc = parser.parseFromString(html, "text/html");
const imgEl = q("#img", doc);
const imgSrc = imgEl == null ? void 0 : imgEl.src;
if (!imgSrc) throw new Error("Image not found");
return imgSrc;
} catch {
if (retries < CFG.maxRetries) {
await new Promise((resolve) => setTimeout(resolve, CFG.retryDelay));
return loadImageWithRetry(url, retries + 1);
}
return null;
}
}
function createRetryHandler(url, placeholder, pIndex, index) {
return () => {
placeholder.className = "r-ph loading";
placeholder.textContent = `P${pIndex}-${index + 1} Reloading...`;
loadImageWithRetry(url).then((newSrc) => {
var _a;
if (newSrc) {
const newImg = document.createElement("img");
newImg.src = newSrc;
newImg.className = "r-img";
(_a = placeholder.parentNode) == null ? void 0 : _a.replaceChild(newImg, placeholder);
}
});
};
}
const prefetchedUrls = new Set();
function prefetchNextPage() {
if (!store.nextUrl || store.nextPagePrefetched || prefetchedUrls.has(store.nextUrl)) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const distanceToBottom = documentHeight - (scrollTop + windowHeight);
if (distanceToBottom < CFG.prefetchDistance) {
store.nextPagePrefetched = true;
prefetchedUrls.add(store.nextUrl);
const parser2 = new DOMParser();
fetch(store.nextUrl).then((r) => r.text()).then((html) => {
const doc = parser2.parseFromString(html, "text/html");
const links = Array.from(qa("#gdt a", doc)).map((a) => a.href);
links.forEach((url) => {
loadImageWithRetry(url).then((imgSrc) => {
if (imgSrc) {
const preloadImg = new Image();
preloadImg.src = imgSrc;
}
}).catch(() => null);
});
}).catch((err) => {
console.error("[Prefetch Failed]", err);
store.nextPagePrefetched = false;
if (store.nextUrl) prefetchedUrls.delete(store.nextUrl);
});
}
}
function setupPrefetchListener() {
let scrollTimer;
window.addEventListener("scroll", () => {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(prefetchNextPage, 200);
}, { passive: true });
}
function setErrorState(placeholder, url, pIndex, index) {
placeholder.className = "r-ph error";
placeholder.innerHTML = `
<div>P${pIndex}-${index + 1} Failed</div>
<button class="retry-btn">Retry</button>
`;
const retryBtn = placeholder.querySelector(".retry-btn");
retryBtn.onclick = createRetryHandler(url, placeholder, pIndex, index);
}
function processBatch(links, pIndex) {
const mainBox = document.querySelector("#gdt");
const batchDiv = document.createElement("div");
batchDiv.className = "page-batch";
const fragment = document.createDocumentFragment();
links.forEach((url, index) => {
const placeholder = document.createElement("div");
placeholder.className = "r-ph loading";
placeholder.textContent = `P${pIndex}-${index + 1} Loading...`;
fragment.appendChild(placeholder);
loadImageWithRetry(url).then((imgSrc) => {
var _a;
if (imgSrc) {
const img = document.createElement("img");
img.className = "r-img";
img.onerror = () => {
if (placeholder.parentNode) {
setErrorState(placeholder, url, pIndex, index);
placeholder.parentNode.replaceChild(placeholder, img);
}
};
img.src = imgSrc;
(_a = placeholder.parentNode) == null ? void 0 : _a.replaceChild(img, placeholder);
} else {
setErrorState(placeholder, url, pIndex, index);
}
}).catch(() => {
placeholder.className = "r-ph error";
placeholder.textContent = `P${pIndex}-${index + 1} Network Error`;
});
});
batchDiv.appendChild(fragment);
mainBox.appendChild(batchDiv);
}
function setupAutoScroll() {
if (!store.settings.autoScroll) return;
const scrollSent = document.createElement("div");
document.body.appendChild(scrollSent);
const parser2 = new DOMParser();
const pageObs = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && store.nextUrl && !store.isFetching && store.settings.autoScroll) {
store.isFetching = true;
fetch(store.nextUrl).then((r) => r.text()).then((html) => {
const doc = parser2.parseFromString(html, "text/html");
const links = Array.from(qa("#gdt a", doc)).map((a) => a.href);
const nUrl = getNextUrl(doc);
store.currPage++;
processBatch(links, store.currPage);
store.nextUrl = nUrl;
store.isFetching = false;
store.nextPagePrefetched = false;
if (!store.nextUrl) pageObs.disconnect();
}).catch(() => {
store.isFetching = false;
});
}
}, { rootMargin: CFG.nextPage });
pageObs.observe(scrollSent);
}
function createScrollbar(onIndexChange) {
const pageIndicator = document.createElement("div");
pageIndicator.className = "sp-scrollbar";
const scrollbarThumb = document.createElement("div");
scrollbarThumb.className = "sp-scrollbar-thumb";
const scrollbarLabel = document.createElement("div");
scrollbarLabel.className = "sp-scrollbar-label";
pageIndicator.appendChild(scrollbarThumb);
pageIndicator.appendChild(scrollbarLabel);
function update() {
if (store.allImages.length === 0) return;
const trackHeight = pageIndicator.offsetHeight;
let thumbHeight;
if (store.allImages.length <= 10) {
thumbHeight = 60;
} else if (store.allImages.length <= 50) {
thumbHeight = Math.max(60, trackHeight * (10 / store.allImages.length));
} else {
thumbHeight = Math.max(60, trackHeight * (5 / store.allImages.length));
}
const scrollProgress = store.currentImageIndex / Math.max(1, store.allImages.length - 1);
const maxThumbTop = trackHeight - thumbHeight;
const thumbTop = scrollProgress * maxThumbTop;
scrollbarThumb.style.height = `${thumbHeight}px`;
scrollbarThumb.style.top = `${thumbTop}px`;
scrollbarLabel.textContent = `${store.currentImageIndex + 1} / ${store.allImages.length}`;
}
pageIndicator.onclick = (e) => {
if (e.target === scrollbarThumb) return;
const rect = pageIndicator.getBoundingClientRect();
const clickY = e.clientY - rect.top;
const scrollProgress = Math.min(1, Math.max(0, clickY / rect.height));
const targetIndex = Math.round(scrollProgress * (store.allImages.length - 1));
if (targetIndex >= 0 && targetIndex < store.allImages.length) {
onIndexChange(targetIndex);
}
};
let isDragging = false;
let dragStartY = 0;
let thumbStartTop = 0;
scrollbarThumb.onmousedown = (e) => {
e.preventDefault();
e.stopPropagation();
isDragging = true;
dragStartY = e.clientY;
thumbStartTop = scrollbarThumb.offsetTop;
document.body.style.userSelect = "none";
};
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const deltaY = e.clientY - dragStartY;
const newTop = thumbStartTop + deltaY;
const trackHeight = pageIndicator.offsetHeight;
const thumbHeight = scrollbarThumb.offsetHeight;
const maxTop = trackHeight - thumbHeight;
const clampedTop = Math.max(0, Math.min(maxTop, newTop));
const scrollProgress = maxTop > 0 ? clampedTop / maxTop : 0;
const targetIndex = Math.round(scrollProgress * (store.allImages.length - 1));
if (targetIndex >= 0 && targetIndex < store.allImages.length && targetIndex !== store.currentImageIndex) {
onIndexChange(targetIndex);
}
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
document.body.style.userSelect = "";
}
});
scrollbarThumb.onclick = (e) => e.stopPropagation();
return { update, getElement: () => pageIndicator };
}
function setupNavigation(deps) {
function nextImage() {
if (store.currentImageIndex < store.allImages.length - 1) {
store.currentImageIndex++;
deps.updateImage();
deps.checkAndLoadNextPage();
} else {
deps.checkAndLoadNextPage();
if (store.settings.autoPlay) {
deps.stopAutoPlayAtEnd();
}
}
}
function previousImage() {
if (store.currentImageIndex > 0) {
store.currentImageIndex--;
deps.updateImage();
if (store.settings.autoPlay) {
deps.resetAutoPlay();
}
}
}
let wheelTimeout;
let wheelDelta = 0;
let isScrolling = false;
const processWheelScroll = () => {
if (!isScrolling) return;
const threshold = 100;
if (Math.abs(wheelDelta) >= threshold) {
if (wheelDelta > 0) {
nextImage();
} else {
previousImage();
}
wheelDelta = wheelDelta > 0 ? wheelDelta - threshold : wheelDelta + threshold;
}
if (isScrolling) {
requestAnimationFrame(processWheelScroll);
}
};
deps.overlay.addEventListener("wheel", (e) => {
e.preventDefault();
wheelDelta += e.deltaY;
if (!isScrolling) {
isScrolling = true;
processWheelScroll();
}
clearTimeout(wheelTimeout);
wheelTimeout = setTimeout(() => {
isScrolling = false;
wheelDelta = 0;
}, 150);
}, { passive: false });
document.addEventListener("keydown", (e) => {
if (!deps.overlay.classList.contains("active")) return;
if (e.key === "Escape") {
deps.closeSinglePageMode();
} else if (e.key === "ArrowDown" || e.key === "ArrowRight") {
nextImage();
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
previousImage();
}
});
return { nextImage, previousImage };
}
function createAutoPlay(nextImageFn) {
function start() {
if (store.autoPlayTimer) clearInterval(store.autoPlayTimer);
if (store.settings.autoPlay) {
store.autoPlayTimer = setInterval(nextImageFn, store.settings.autoPlayInterval);
}
}
function stop() {
if (store.autoPlayTimer) {
clearInterval(store.autoPlayTimer);
store.autoPlayTimer = null;
}
}
function reset() {
if (store.settings.autoPlay) {
stop();
start();
}
}
function stopAtEnd() {
store.updateSetting("autoPlay", false);
stop();
}
return { start, stop, reset, stopAtEnd };
}
function createSinglePageOverlay(deps) {
const overlay = document.createElement("div");
overlay.className = "single-page-overlay";
const closeBtn = document.createElement("div");
closeBtn.className = "sp-close-btn";
closeBtn.innerHTML = "✕";
const imageContainer = document.createElement("div");
imageContainer.className = "sp-image-container";
const currentImage = document.createElement("img");
currentImage.className = "sp-current-image";
imageContainer.appendChild(currentImage);
let loadPollTimer = null;
function isImageReady(img) {
return !!(img && img.src && !img.src.includes("data:") && img.complete && img.naturalWidth > 0);
}
function clearLoadPoll() {
if (loadPollTimer) {
clearInterval(loadPollTimer);
loadPollTimer = null;
}
}
function showPlaceholder() {
currentImage.style.display = "none";
const existing = imageContainer.querySelector(".sp-placeholder");
if (existing) existing.remove();
const ph = document.createElement("div");
ph.className = "sp-placeholder";
ph.innerHTML = `<div class="sp-placeholder-pulse"></div><div class="sp-placeholder-text">${store.currentImageIndex + 1} / ${store.allImages.length}</div>`;
imageContainer.appendChild(ph);
}
function removePlaceholder() {
const ph = imageContainer.querySelector(".sp-placeholder");
if (ph) ph.remove();
currentImage.style.display = "";
}
function updateImage() {
clearLoadPoll();
const idx = store.currentImageIndex;
const img = store.allImages[idx];
if (!img) {
showPlaceholder();
scrollbar.update();
startLoadPoll(idx);
return;
}
if (isImageReady(img)) {
removePlaceholder();
currentImage.src = img.src;
scrollbar.update();
return;
}
showPlaceholder();
scrollbar.update();
startLoadPoll(idx);
}
function startLoadPoll(idx) {
const wasAutoPlaying = !!store.autoPlayTimer;
if (wasAutoPlaying) autoPlay.stop();
loadPollTimer = setInterval(() => {
if (store.currentImageIndex !== idx) {
clearLoadPoll();
return;
}
const img = store.allImages[idx];
if (img && isImageReady(img)) {
clearLoadPoll();
removePlaceholder();
currentImage.src = img.src;
scrollbar.update();
if (wasAutoPlaying && store.settings.autoPlay) autoPlay.start();
}
}, 200);
}
const autoPlay = createAutoPlay(() => nav.nextImage());
const scrollbar = createScrollbar((index) => {
store.currentImageIndex = index;
updateImage();
autoPlay.reset();
});
const nav = setupNavigation({
overlay,
updateImage,
checkAndLoadNextPage: () => checkAndLoadNextPage(),
resetAutoPlay: () => autoPlay.reset(),
stopAutoPlayAtEnd: () => autoPlay.stopAtEnd(),
closeSinglePageMode: () => close()
});
overlay.appendChild(closeBtn);
overlay.appendChild(scrollbar.getElement());
overlay.appendChild(imageContainer);
document.body.appendChild(overlay);
closeBtn.onclick = () => close();
function open() {
store.allImages = Array.from(qa(".r-img"));
if (store.allImages.length === 0) {
alert("Please wait for images to load");
return;
}
const viewportCenter = window.scrollY + window.innerHeight / 2;
const searchRange = window.innerHeight * 2;
let closestIndex = 0;
let minDistance = Infinity;
store.allImages.forEach((img, index) => {
const rect = img.getBoundingClientRect();
const imgTop = rect.top + window.scrollY;
if (Math.abs(imgTop - viewportCenter) < searchRange) {
const imgCenter = imgTop + rect.height / 2;
const distance = Math.abs(imgCenter - viewportCenter);
if (distance < minDistance) {
minDistance = distance;
closestIndex = index;
}
}
});
store.currentImageIndex = closestIndex;
overlay.classList.add("active");
document.body.style.overflow = "hidden";
updateImage();
if (store.settings.autoPlay) {
autoPlay.start();
}
}
function close() {
clearLoadPoll();
autoPlay.stop();
overlay.classList.remove("active");
document.body.style.overflow = "";
const currentImages = Array.from(qa(".r-img"));
if (store.currentImageIndex >= 0 && store.currentImageIndex < currentImages.length) {
const targetImg = currentImages[store.currentImageIndex];
if (targetImg) {
setTimeout(() => {
targetImg.scrollIntoView({ behavior: "smooth", block: "center" });
}, 100);
}
}
}
store.on("settingsChanged", () => {
if (!overlay.classList.contains("active")) return;
if (store.settings.autoPlay) {
autoPlay.start();
} else {
autoPlay.stop();
}
});
function checkAndLoadNextPage() {
if (!store.settings.autoScroll || !store.nextUrl || store.isFetching) return;
const remainingImages = store.allImages.length - store.currentImageIndex;
if (remainingImages <= 10) {
store.isFetching = true;
const parser2 = new DOMParser();
fetch(store.nextUrl).then((r) => r.text()).then((html) => {
const doc = parser2.parseFromString(html, "text/html");
const links = Array.from(qa("#gdt a", doc)).map((a) => a.href);
deps.onLoadNextPage(links, doc);
const mainBox = document.querySelector("#gdt");
if (mainBox) {
const prevCount = store.allImages.length;
const obs = new MutationObserver(() => {
const newImages = Array.from(qa(".r-img"));
if (newImages.length > prevCount) {
store.allImages = newImages;
scrollbar.update();
obs.disconnect();
}
});
obs.observe(mainBox, { childList: true, subtree: true });
setTimeout(() => obs.disconnect(), 1e4);
}
store.nextUrl = getNextUrl(doc);
store.isFetching = false;
store.nextPagePrefetched = false;
}).catch((err) => {
console.error("[Single Page] Load failed", err);
store.isFetching = false;
});
}
}
return {
open,
close,
isActive: () => overlay.classList.contains("active"),
getOverlayElement: () => overlay
};
}
function initSinglePageMode() {
const spm = createSinglePageOverlay({
onLoadNextPage: (links, doc) => {
store.currPage++;
processBatch(links, store.currPage);
store.nextUrl = getNextUrl(doc);
}
});
return spm;
}
const svgSettings = `<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>`;
const svgReader = `<svg viewBox="0 0 24 24"><path d="M21 5c-1.11-.35-2.33-.5-3.5-.5-1.95 0-4.05.4-5.5 1.5-1.45-1.1-3.55-1.5-5.5-1.5S2.45 4.9 1 6v14.65c0 .25.25.5.5.5.1 0 .15-.05.25-.05C3.1 20.45 5.05 20 6.5 20c1.95 0 4.05.4 5.5 1.5 1.35-.85 3.8-1.5 5.5-1.5 1.65 0 3.35.3 4.75 1.05.1.05.15.05.25.05.25 0 .5-.25.5-.5V6c-.6-.45-1.25-.75-2-1zm0 13.5c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5V8c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5v11.5z"/></svg>`;
const svgPlay = `<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>`;
const svgPause = `<svg viewBox="0 0 24 24"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>`;
const SETTINGS = [
{ label: "Show Control", key: "showControl" },
{ label: "Auto Scroll", key: "autoScroll" },
{ label: "Auto Enter Reader", key: "autoEnterSinglePage" }
];
function createSettingsPanel() {
const settingsBtn = document.createElement("div");
settingsBtn.className = "settings-btn";
const settingsPanel = document.createElement("div");
settingsPanel.className = "settings-panel";
SETTINGS.forEach(({ label, key }) => {
const item = document.createElement("div");
item.className = "settings-item";
const labelEl = document.createElement("span");
labelEl.className = "settings-label";
labelEl.textContent = label;
const toggle = document.createElement("div");
toggle.className = `toggle-switch${store.settings[key] ? " on" : ""}`;
const slider = document.createElement("div");
slider.className = "toggle-slider";
toggle.appendChild(slider);
toggle.onclick = () => {
const newValue = !store.settings[key];
store.updateSetting(key, newValue);
toggle.classList.toggle("on", newValue);
};
item.appendChild(labelEl);
item.appendChild(toggle);
settingsPanel.appendChild(item);
});
const intervalItem = document.createElement("div");
intervalItem.className = "settings-item";
const intervalLabel = document.createElement("span");
intervalLabel.className = "settings-label";
intervalLabel.textContent = "Play Interval";
const intervalRight = document.createElement("div");
intervalRight.style.cssText = "display:flex;align-items:center;gap:4px;";
const intervalInput = document.createElement("input");
intervalInput.type = "number";
intervalInput.className = "interval-input";
intervalInput.min = "1";
intervalInput.max = "60";
intervalInput.step = "0.5";
intervalInput.value = String(store.settings.autoPlayInterval / 1e3);
intervalInput.onclick = (e) => e.stopPropagation();
intervalInput.onchange = (e) => {
const value = parseFloat(e.target.value);
if (!isNaN(value) && value >= 1 && value <= 60) {
store.updateSetting("autoPlayInterval", value * 1e3);
}
};
const intervalUnit = document.createElement("span");
intervalUnit.textContent = "s";
intervalUnit.style.cssText = "font-size:12px;color:#888;";
intervalRight.appendChild(intervalInput);
intervalRight.appendChild(intervalUnit);
intervalItem.appendChild(intervalLabel);
intervalItem.appendChild(intervalRight);
settingsPanel.appendChild(intervalItem);
settingsBtn.onclick = (e) => {
e.stopPropagation();
settingsPanel.classList.toggle("show");
};
document.addEventListener("click", (e) => {
if (!settingsPanel.contains(e.target) && !settingsBtn.contains(e.target)) {
settingsPanel.classList.remove("show");
}
});
return {
getButtonElement: () => settingsBtn,
getPanelElement: () => settingsPanel
};
}
function createFloatControl(spmHandle) {
const floatControl = document.createElement("div");
floatControl.className = `float-control${store.settings.showControl ? "" : " hidden"}`;
const autoPlayBtn = document.createElement("div");
autoPlayBtn.className = `side-btn auto-play-btn hidden${store.settings.autoPlay ? " active" : ""}`;
autoPlayBtn.innerHTML = store.settings.autoPlay ? svgPause : svgPlay;
autoPlayBtn.title = "Auto Play";
autoPlayBtn.onclick = (e) => {
e.stopPropagation();
const newValue = !store.settings.autoPlay;
store.updateSetting("autoPlay", newValue);
autoPlayBtn.innerHTML = newValue ? svgPause : svgPlay;
autoPlayBtn.classList.toggle("active", newValue);
};
const circleControl = document.createElement("div");
circleControl.className = "circle-control";
circleControl.innerHTML = svgReader;
circleControl.title = "Reader Mode";
circleControl.onclick = () => {
if (spmHandle.isActive()) {
spmHandle.close();
autoPlayBtn.classList.add("hidden");
} else {
spmHandle.open();
autoPlayBtn.classList.remove("hidden");
autoPlayBtn.innerHTML = store.settings.autoPlay ? svgPause : svgPlay;
autoPlayBtn.classList.toggle("active", store.settings.autoPlay);
}
};
const settings = createSettingsPanel();
const settingsBtn = settings.getButtonElement();
settingsBtn.className = "side-btn";
settingsBtn.innerHTML = svgSettings;
settingsBtn.title = "Settings";
floatControl.appendChild(autoPlayBtn);
floatControl.appendChild(circleControl);
floatControl.appendChild(settingsBtn);
floatControl.appendChild(settings.getPanelElement());
document.body.appendChild(floatControl);
}
function registerMenuCommands() {
_GM_registerMenuCommand("Toggle Auto Scroll", () => {
store.updateSetting("autoScroll", !store.settings.autoScroll);
alert(`Auto Scroll ${store.settings.autoScroll ? "Enabled" : "Disabled"}`);
location.reload();
});
_GM_registerMenuCommand("Toggle Control Display", () => {
store.updateSetting("showControl", !store.settings.showControl);
alert(`Control Display ${store.settings.showControl ? "Enabled" : "Disabled"}`);
location.reload();
});
_GM_registerMenuCommand("Toggle Auto Enter Single Page", () => {
store.updateSetting("autoEnterSinglePage", !store.settings.autoEnterSinglePage);
alert(`Auto Enter Single Page ${store.settings.autoEnterSinglePage ? "Enabled" : "Disabled"}`);
location.reload();
});
}
(function main() {
hideOriginalElements();
const mainBox = document.querySelector("#gdt");
if (!mainBox) return;
const urlP = new URLSearchParams(window.location.search).get("p");
store.currPage = urlP ? parseInt(urlP) + 1 : 1;
const initLinks = Array.from(qa("#gdt a", document)).map((a) => a.href);
const galleryId = window.location.pathname;
const savedTotal = localStorage.getItem(`eh_total_${galleryId}`);
if (savedTotal && parseInt(savedTotal) > 0) {
store.totalPage = parseInt(savedTotal);
} else {
store.totalPage = calcTotal(document, initLinks.length);
localStorage.setItem(`eh_total_${galleryId}`, String(store.totalPage));
}
store.nextUrl = getNextUrl(document);
mainBox.innerHTML = "";
processBatch(initLinks, store.currPage);
let spmHandle;
createFloatControl({
open: () => spmHandle.open(),
close: () => spmHandle.close(),
isActive: () => spmHandle.isActive(),
getOverlayElement: () => spmHandle.getOverlayElement()
});
spmHandle = initSinglePageMode();
setupAutoScroll();
setupPrefetchListener();
registerMenuCommands();
if (store.settings.autoEnterSinglePage) {
setTimeout(() => spmHandle.open(), 1e3);
}
})();
})();