e-hentai Plus

Infinite scroll & reader mode with image prefetch and floating controls for E-Hentai / ExHentai

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name               e-hentai Plus
// @name:zh-CN         E-Hentai 增强阅读
// @namespace          http://tampermonkey.net/
// @version            2.2.1
// @author             Viki
// @description        Infinite scroll & reader mode with image prefetch and floating controls for E-Hentai / ExHentai
// @description:zh-CN  为 E-Hentai / ExHentai 提供无限滚动和阅读模式,支持图片预取与悬浮控制
// @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 hasLoadingPlaceholders() {
      return document.querySelectorAll(".r-ph").length > 0;
    }
    function syncAllImages() {
      const freshImages = Array.from(qa(".r-img"));
      if (freshImages.length !== store.allImages.length) {
        store.allImages = freshImages;
      }
    }
    function nextImage() {
      syncAllImages();
      if (store.currentImageIndex < store.allImages.length - 1) {
        store.currentImageIndex++;
        deps.updateImage();
        deps.checkAndLoadNextPage();
      } else if (hasLoadingPlaceholders()) {
        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 = "&#10005;";
    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 freshImages = Array.from(qa(".r-img"));
        if (freshImages.length !== store.allImages.length) {
          store.allImages = freshImages;
          scrollbar.update();
        }
        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 expectedTotal = store.allImages.length + links.length;
            const obs = new MutationObserver(() => {
              const newImages = Array.from(qa(".r-img"));
              if (newImages.length !== store.allImages.length) {
                store.allImages = newImages;
                scrollbar.update();
              }
              if (newImages.length >= expectedTotal) {
                obs.disconnect();
              }
            });
            obs.observe(mainBox, { childList: true, subtree: true });
            setTimeout(() => {
              obs.disconnect();
              const finalImages = Array.from(qa(".r-img"));
              if (finalImages.length !== store.allImages.length) {
                store.allImages = finalImages;
                scrollbar.update();
              }
            }, 3e4);
          }
          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);
    }
  })();

})();