nhentai Infinite Scroll

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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');

})();