e-hentai Plus

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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);
    }
  })();

})();