nhentai Infinite Scroll

Dynamically loads more comics as you scroll on nhentai.net (including favorites)

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला 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         nhentai Infinite Scroll
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Dynamically loads more comics as you scroll on nhentai.net (including favorites)
// @author       Hentiedup (original), [Snow2122] (adaptation)
// @license      MIT
// @match        https://nhentai.net/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @grant        GM_getValue
// @grant        GM_addStyle
// @noframes
// ==/UserScript==

(function() {
    'use strict';

    // --- SETTINGS ---
    const infinite_load = GM_getValue("infinite_load", true);
    if (!infinite_load) return;

    // --- STATE ---
    let isLoadingNextPage = false;
    let autoLoadInterval  = null;
    let currentPageKey    = null;

    // --- HELPERS ---

    function isFavoritesPage() {
        return window.location.pathname.startsWith("/user/favorites");
    }

    /**
     * Finds the gallery container using a fallback chain.
     *
     * Different page types use different container classes on nhentai:
     *   - Tag / artist / etc. pages  →  .container.index-container
     *   - Search result pages        →  may use a different class (no .container)
     *
     * We try selectors from most-specific to least-specific, then fall back
     * to using the parentElement of any gallery item that isn't in a special
     * section (popular / advertisement).
     */
    function getGalleryContainer() {
        if (isFavoritesPage()) return $("#favcontainer");

        // Level 1 — standard listing pages (home, tag, artist, character, …)
        let $c = $(".container.index-container:not(.advertisement, .index-popular)").first();
        if ($c.length) return $c;

        // Level 2 — search result pages (may omit the .container class)
        $c = $(".index-container:not(.advertisement, .index-popular)").first();
        if ($c.length) return $c;

        // Level 3 — last resort: whatever element wraps the first gallery item
        //            that isn't inside a special section.
        $c = $("div.gallery")
                .not(".index-popular div.gallery, .advertisement div.gallery")
                .first()
                .parent();
        return $c; // may be an empty jQuery object — caller checks .length
    }

    /**
     * Extracts gallery items from a fetched page document.
     * Uses the same fallback chain as getGalleryContainer().
     */
    function extractGalleries($doc) {
        // Level 1
        let $items = $doc.find(".container.index-container:not(.advertisement, .index-popular) div.gallery");
        if ($items.length) return $items;

        // Level 2
        $items = $doc.find(".index-container:not(.advertisement, .index-popular) div.gallery");
        if ($items.length) return $items;

        // Level 3 — any gallery not inside a special section
        return $doc.find("div.gallery").not(
            $doc.find(".index-popular div.gallery, .advertisement div.gallery")
        );
    }

    function getCurrentPageNum() {
        const href  = $("section.desktop-pagination a.page.current").last().attr("href") || "";
        const match = href.match(/[?&]page=(\d+)/);
        return match ? parseInt(match[1], 10) : 1;
    }

    function getLastPageNum() {
        const href  = $("section.desktop-pagination a.last").first().attr("href") || "";
        const match = href.match(/[?&]page=(\d+)/);
        return match ? parseInt(match[1], 10) : 0; // 0 = not yet rendered
    }

    // --- STYLES ---
    function addStyles() {
        GM_addStyle(`
            #NHI_loader_icon {
                height: 355px;
                line-height: 355px;
                width: 100%;
                text-align: center;
            }
            #NHI_loader_icon > div { display: inline-flex; }
            .loader {
                color: #ed2553;
                font-size: 10px;
                width: 1em; height: 1em;
                border-radius: 50%;
                position: relative;
                text-indent: -9999em;
                animation: mulShdSpin 1.3s infinite linear;
                transform: translateZ(0);
            }
            @keyframes mulShdSpin {
                0%,100%{ box-shadow:0 -3em 0 .2em,2em -2em 0 0,3em 0 0 -1em,2em 2em 0 -1em,0 3em 0 -1em,-2em 2em 0 -1em,-3em 0 0 -1em,-2em -2em 0 0; }
                12.5%  { box-shadow:0 -3em 0 0,2em -2em 0 .2em,3em 0 0 0,2em 2em 0 -1em,0 3em 0 -1em,-2em 2em 0 -1em,-3em 0 0 -1em,-2em -2em 0 -1em; }
                25%    { box-shadow:0 -3em 0 -.5em,2em -2em 0 0,3em 0 0 .2em,2em 2em 0 0,0 3em 0 -1em,-2em 2em 0 -1em,-3em 0 0 -1em,-2em -2em 0 -1em; }
                37.5%  { box-shadow:0 -3em 0 -1em,2em -2em 0 -1em,3em 0 0 0,2em 2em 0 .2em,0 3em 0 0,-2em 2em 0 -1em,-3em 0 0 -1em,-2em -2em 0 -1em; }
                50%    { box-shadow:0 -3em 0 -1em,2em -2em 0 -1em,3em 0 0 -1em,2em 2em 0 0,0 3em 0 .2em,-2em 2em 0 0,-3em 0 0 -1em,-2em -2em 0 -1em; }
                62.5%  { box-shadow:0 -3em 0 -1em,2em -2em 0 -1em,3em 0 0 -1em,2em 2em 0 -1em,0 3em 0 0,-2em 2em 0 .2em,-3em 0 0 0,-2em -2em 0 -1em; }
                75%    { box-shadow:0 -3em 0 -1em,2em -2em 0 -1em,3em 0 0 -1em,2em 2em 0 -1em,0 3em 0 -1em,-2em 2em 0 0,-3em 0 0 .2em,-2em -2em 0 0; }
                87.5%  { box-shadow:0 -3em 0 0,2em -2em 0 -1em,3em 0 0 -1em,2em 2em 0 -1em,0 3em 0 -1em,-2em 2em 0 0,-3em 0 0 0,-2em -2em 0 .2em; }
            }
        `);
    }

    // --- FETCH AND APPEND ---

    function tryLoadInNextPageComics(pageNumToLoad, fetchUrlWithoutPageNum, retryNum, maxRetries) {
        retryNum  = retryNum  || 0;
        maxRetries = maxRetries || 5;

        if (retryNum === 0 && isLoadingNextPage) return;

        // Read lastPageNum fresh every call so we always use the current page's value.
        const lastPageNum = getLastPageNum();
        if (lastPageNum > 0 && pageNumToLoad > lastPageNum) return;

        isLoadingNextPage = true;

        const galleryContainer = getGalleryContainer();

        if ($("#NHI_loader_icon").length === 0) {
            galleryContainer.append(
                '<div id="NHI_loader_icon" class="gallery"><div><span class="loader"></span></div></div>'
            );
        }

        $.get({ url: fetchUrlWithoutPageNum + pageNumToLoad, dataType: "html" }, (data) => {
            const $doc = $(data);

            if (isFavoritesPage()) {
                $doc.find("div.gallery-favorite").each((i, el) => {
                    const $el       = $(el);
                    const coverHref = $el.find(".gallery .cover").attr("href");
                    if ($(`.cover[href='${coverHref}']`, galleryContainer).length) return;
                    galleryContainer.append($el);
                });
            } else {
                extractGalleries($doc).each((i, el) => {
                    const $el       = $(el);
                    const coverHref = $el.find(".cover").attr("href");
                    if ($(`.cover[href='${coverHref}']`, galleryContainer).length) return;
                    galleryContainer.append($el);
                });
            }

            // Update desktop paginator to reflect new current page.
            const $desktopPag = $("section.desktop-pagination");
            const $pageLink   = $desktopPag.find(`a.page[href$='page=${pageNumToLoad}']`);
            if ($pageLink.length) {
                $pageLink.addClass("current");
            } else {
                $desktopPag.find("a.next").before(
                    `<a href="${fetchUrlWithoutPageNum}${pageNumToLoad}" class="page current">${pageNumToLoad}</a>`
                );
            }

            // Update mobile paginator if present.
            $("section.mobile-pagination")
                .find(`a.page[href$='page=${pageNumToLoad}']`)
                .addClass("current");

            $("#NHI_loader_icon").remove();
            isLoadingNextPage = false;

        }).fail(() => {
            if (retryNum < maxRetries) {
                console.log(`[NHI] Page ${pageNumToLoad} failed — retry ${retryNum + 1}/${maxRetries}`);
                setTimeout(() => {
                    tryLoadInNextPageComics(pageNumToLoad, fetchUrlWithoutPageNum, retryNum + 1, maxRetries);
                }, 1000);
            } else {
                console.log(`[NHI] Page ${pageNumToLoad} — giving up after ${maxRetries} retries.`);
                $("#NHI_loader_icon").remove();
                isLoadingNextPage = false;
            }
        });
    }

    // --- PAGE INITIALISATION ---

    function initPage() {
        // Build a stable key: pathname + query string, minus any page= param.
        const rawSearch = window.location.search
            .replace(/[?&]page=\d+/, "")
            .replace(/^&/, "?");
        const pageKey = window.location.pathname + rawSearch;

        // Already initialised for this exact page — skip.
        if (pageKey === currentPageKey) return;

        // The gallery container must exist before we can initialise.
        // NOTE: we do NOT require section.desktop-pagination here because on
        // some page types (e.g. search) it may render slightly after the gallery.
        // The scroll listener and interval read lastPageNum dynamically instead.
        const $gallery = getGalleryContainer();
        if (!$gallery.length) return;

        // Mark as initialised now so concurrent retry passes don't double-init.
        currentPageKey = pageKey;

        // Tear down previous page's state.
        isLoadingNextPage = false;
        $(window).off("scroll.nhi");
        if (autoLoadInterval !== null) {
            clearInterval(autoLoadInterval);
            autoLoadInterval = null;
        }
        $("#NHI_loader_icon").remove();

        // Base URL for fetching subsequent pages.
        // rawSearch already excludes any existing page= param.
        const finalUrlWithoutPageNum =
            window.location.pathname +
            rawSearch +
            (rawSearch.length ? "&" : "?") +
            "page=";

        console.log(`[NHI] Initialised → ${pageKey} | fetch base: ${finalUrlWithoutPageNum}`);

        // ------------------------------------------------------------------
        // SCROLL LISTENER
        // lastPageNum and getCurrentPageNum() are read fresh on every scroll
        // event so they always reflect the current page's paginator, never
        // a value captured from a previous page.
        // ------------------------------------------------------------------
        $(window).on("scroll.nhi", () => {
            const scrolledTo = $(window).scrollTop() + (window.visualViewport?.height || $(window).height());
            if (scrolledTo >= $(document).height() - 500) {
                const last = getLastPageNum();
                if (last === 0) return; // paginator not rendered yet
                tryLoadInNextPageComics(getCurrentPageNum() + 1, finalUrlWithoutPageNum);
            }
        });

        // ------------------------------------------------------------------
        // AUTO-FILL INTERVAL
        // Keeps loading pages while the page is too short to produce a scrollbar.
        // Waits for paginator a.last to render before checking page counts.
        // ------------------------------------------------------------------
        autoLoadInterval = setInterval(() => {
            const last = getLastPageNum();
            if (last === 0) return; // a.last not rendered yet — wait

            const next = getCurrentPageNum() + 1;
            if (next > last) {
                clearInterval(autoLoadInterval);
                autoLoadInterval = null;
                return;
            }

            const doc = document.documentElement;
            if (doc.scrollHeight <= doc.clientHeight) {
                tryLoadInNextPageComics(next, finalUrlWithoutPageNum);
            } else {
                clearInterval(autoLoadInterval);
                autoLoadInterval = null;
            }
        }, 200);
    }

    // --- SPA NAVIGATION DETECTION ---

    function onNavigate() {
        // Reset key so the next initPage() call re-initialises for the new URL.
        currentPageKey = null;

        // Schedule multiple attempts at staggered delays.
        // Earlier passes catch fast renders; later passes catch slow ones.
        // Starting at 200 ms avoids reading stale DOM from the previous page.
        [200, 450, 800, 1300, 2200, 3500].forEach((d) => setTimeout(initPage, d));
    }

    (function patchHistory() {
        const wrap = (orig) => function (...args) {
            orig.apply(this, args);
            onNavigate();
        };
        history.pushState    = wrap(history.pushState.bind(history));
        history.replaceState = wrap(history.replaceState.bind(history));
        window.addEventListener("popstate", onNavigate);
    })();

    // --- BOOT ---
    addStyles();

    // Initial page load: try immediately and at short intervals in case
    // SvelteKit hasn't rendered the gallery yet.
    [0, 150, 400, 800, 1500].forEach((d) => setTimeout(initPage, d));

    console.log('[NHentai - Infinite Scroll] UserScript loaded. v3.1');

})();