nhentai Vertical Scroll

View nhentai galleries in a vertical scroll instead of clicking page by page

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         nhentai Vertical Scroll
// @namespace    https://nhentai.net
// @version      1.4
// @author       JoyArz
// @description  View nhentai galleries in a vertical scroll instead of clicking page by page
// @match        https://nhentai.net/g/*/
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const STYLE = `
        body {
            background: #0f0f14 !important;
        }
        #content {
            background: #0f0f14 !important;
        }
        .vs-container {
            position: relative;
            width: 100%;
            max-width: 100vw;
            margin: 0 auto;
            padding-top: 20px;
        }
        .vs-page {
            position: relative;
            width: 100%;
            display: flex;
            justify-content: center;
            background: #0f0f14;
            margin-bottom: 8px;
            min-height: 100px;
        }
        .vs-page img {
            max-width: 100%;
            max-height: 100vh;
            object-fit: contain;
            display: block;
        }
        .vs-page img.loading {
            opacity: 0.3;
        }
        .vs-loading {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0, 0, 0, 0.9);
            color: white;
            padding: 20px 40px;
            border-radius: 10px;
            font-family: sans-serif;
            font-size: 16px;
            z-index: 9999;
            text-align: center;
        }
        .vs-nav {
            position: fixed;
            bottom: 20px;
            right: 20px;
            display: flex;
            flex-direction: column;
            gap: 10px;
            z-index: 9998;
        }
        .vs-nav button {
            width: 50px;
            height: 50px;
            border-radius: 50%;
            border: none;
            background: #ff3366;
            color: white;
            font-size: 20px;
            cursor: pointer;
            opacity: 0.8;
            transition: opacity 0.2s, transform 0.2s;
        }
        .vs-nav button:hover {
            opacity: 1;
            transform: scale(1.1);
        }
        .vs-nav-top {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 9998;
        }
        .vs-nav-top button {
            padding: 10px 20px;
            border-radius: 20px;
            border: none;
            background: #ff3366;
            color: white;
            font-size: 14px;
            cursor: pointer;
            opacity: 0.8;
            transition: opacity 0.2s;
        }
        .vs-nav-top button:hover {
            opacity: 1;
        }
        .vs-error {
            color: #ff3366;
            padding: 20px;
            text-align: center;
        }
        .hidden { display: none !important; }
    `;

    const NAV_HTML = `
        <div class="vs-nav">
            <button id="vs-scroll-top" title="Scroll to top">▲</button>
            <button id="vs-scroll-bottom" title="Scroll to bottom">▼</button>
        </div>
        <div class="vs-nav-top">
            <button id="vs-toggle-zoom">Zoom: 1.0x</button>
        </div>
    `;

    class NHentaiVerticalScroll {
        constructor() {
            this.galleryId = null;
            this.pages = 0;
            this.currentZoom = 1.0;
            this.container = null;
            this.loadedPages = new Set();
            this.loadedCount = 0;
            this.initialized = false;
        }

        extractGalleryInfo() {
            const thumbs = document.querySelectorAll('#thumbnail-container .thumb-container');
            if (thumbs.length > 0) {
                this.pages = thumbs.length;
                const firstThumb = thumbs[0].querySelector('img');
                if (firstThumb) {
                    const srcMatch = firstThumb.src.match(/\/galleries\/(\d+)\//);
                    if (srcMatch) {
                        this.galleryId = srcMatch[1];
                    }
                }
            }

            if (!this.galleryId) {
                const coverImg = document.querySelector('#cover img');
                if (coverImg) {
                    const srcMatch = coverImg.src.match(/\/galleries\/(\d+)\//);
                    if (srcMatch) {
                        this.galleryId = srcMatch[1];
                    }
                }
            }

            console.log('NHentai VS:', { galleryId: this.galleryId, pages: this.pages });
        }

        getImageUrl(pageNum) {
            const servers = ['i1', 'i2', 'i3', 'i4'];
            const server = servers[(pageNum - 1) % 4];
            return `https://${server}.nhentai.net/galleries/${this.galleryId}/${pageNum}.webp`;
        }

        init() {
            if (this.initialized) return;
            this.initialized = true;

            this.extractGalleryInfo();

            if (!this.galleryId || !this.pages) {
                this.showError('Could not detect gallery info');
                return;
            }

            this.injectStyles();
            this.createVerticalView();
            this.addNavigation();
            this.setupKeyboardNav();
            this.loadVisiblePages();
        }

        showError(msg) {
            const errorDiv = document.createElement('div');
            errorDiv.className = 'vs-error';
            errorDiv.textContent = msg;
            const content = document.querySelector('#content');
            if (content) {
                content.innerHTML = '';
                content.appendChild(errorDiv);
            }
        }

        injectStyles() {
            const styleEl = document.createElement('style');
            styleEl.id = 'vs-styles';
            styleEl.textContent = STYLE;
            document.head.appendChild(styleEl);
        }

        createVerticalView() {
            const thumbnailContainer = document.querySelector('#thumbnail-container');
            if (thumbnailContainer) {
                thumbnailContainer.classList.add('hidden');
            }

            this.container = document.createElement('div');
            this.container.className = 'vs-container';

            for (let i = 1; i <= this.pages; i++) {
                const pageEl = document.createElement('div');
                pageEl.className = 'vs-page';
                pageEl.dataset.page = i;
                pageEl.innerHTML = `<img class="loading" data-src="${this.getImageUrl(i)}" alt="Page ${i}">`;
                this.container.appendChild(pageEl);
            }

            const bigcontainer = document.querySelector('#bigcontainer');
            const relatedContainer = document.querySelector('#related-container');
            const commentPostContainer = document.querySelector('#comment-post-container');
            const commentContainer = document.querySelector('#comment-container');

            if (relatedContainer && relatedContainer.parentNode) {
                relatedContainer.parentNode.insertBefore(this.container, relatedContainer);
            } else if (commentPostContainer && commentPostContainer.parentNode) {
                commentPostContainer.parentNode.insertBefore(this.container, commentPostContainer);
            } else if (commentContainer && commentContainer.parentNode) {
                commentContainer.parentNode.insertBefore(this.container, commentContainer);
            } else {
                const content = document.querySelector('#content');
                if (content) {
                    content.appendChild(this.container);
                }
            }

            const loading = document.createElement('div');
            loading.className = 'vs-loading';
            loading.id = 'vs-loading';
            loading.textContent = `Loading...`;
            document.body.appendChild(loading);

            setTimeout(() => {
                this.loadInitialBatch();
            }, 500);
        }

        loadInitialBatch() {
            const loading = document.getElementById('vs-loading');
            const pages = Array.from(this.container.querySelectorAll('.vs-page'));
            const batchSize = 10;
            let loaded = 0;

            const toLoad = pages.slice(0, Math.min(batchSize, this.pages));

            toLoad.forEach((pageEl) => {
                const img = pageEl.querySelector('img');
                const pageNum = parseInt(pageEl.dataset.page, 10);

                img.onload = () => {
                    img.classList.remove('loading');
                    this.loadedPages.add(pageNum);
                    this.loadedCount++;
                    loaded++;
                    if (loading) loading.textContent = `Loaded ${this.loadedCount}/${this.pages}`;
                    if (loaded >= toLoad.length) {
                        setTimeout(() => { if (loading) loading.classList.add('hidden'); }, 300);
                    }
                };

                img.onerror = () => {
                    img.classList.remove('loading');
                    img.alt = `Failed page ${pageNum}`;
                    this.loadedPages.add(pageNum);
                    this.loadedCount++;
                    loaded++;
                    if (loaded >= toLoad.length) {
                        setTimeout(() => { if (loading) loading.classList.add('hidden'); }, 300);
                    }
                };

                img.src = img.dataset.src;
            });
        }

        loadVisiblePages() {
            if (!this.container) return;

            const viewportHeight = window.innerHeight;
            const scrollY = window.scrollY;
            const pages = Array.from(this.container.querySelectorAll('.vs-page'));
            const currentPage = Math.floor((scrollY + viewportHeight) / viewportHeight);

            pages.forEach(pageEl => {
                const pageNum = parseInt(pageEl.dataset.page, 10);
                if (this.loadedPages.has(pageNum)) return;

                if (pageNum <= currentPage + 3) {
                    const img = pageEl.querySelector('img');
                    if (img && img.dataset.src) {
                        img.src = img.dataset.src;
                        this.loadedPages.add(pageNum);

                        img.onload = () => img.classList.remove('loading');
                        img.onerror = () => {
                            img.classList.remove('loading');
                            img.alt = `Failed page ${pageNum}`;
                        };
                    }
                }
            });
        }

        addNavigation() {
            const navDiv = document.createElement('div');
            navDiv.innerHTML = NAV_HTML;
            document.body.appendChild(navDiv);

            document.getElementById('vs-scroll-top').addEventListener('click', () => {
                window.scrollTo({ top: 0, behavior: 'smooth' });
            });

            document.getElementById('vs-scroll-bottom').addEventListener('click', () => {
                window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
            });

            document.getElementById('vs-toggle-zoom').addEventListener('click', (e) => {
                this.toggleZoom(e.target);
            });
        }

        toggleZoom(btn) {
            this.currentZoom = this.currentZoom === 1.0 ? 1.5 : 1.0;
            document.querySelectorAll('.vs-page img').forEach(img => {
                img.style.maxHeight = `${100 * this.currentZoom}vh`;
            });
            if (btn) btn.textContent = `Zoom: ${this.currentZoom}x`;
        }

        setupKeyboardNav() {
            window.addEventListener('scroll', () => {
                this.loadVisiblePages();
            }, { passive: true });

            setInterval(() => {
                this.loadVisiblePages();
            }, 500);
        }
    }

    function initScript() {
        if (document.querySelector('#vs-styles')) return;

        const vs = new NHentaiVerticalScroll();

        const observer = new MutationObserver((mutations) => {
            let shouldInit = false;
            for (const mutation of mutations) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeName === 'SECTION' || node.nodeName === 'DIV') {
                            if (node.id === 'thumbnail-container' || node.querySelector?.('#thumbnail-container')) {
                                shouldInit = true;
                                break;
                            }
                        }
                    }
                }
            }
            if (shouldInit && !vs.initialized) {
                setTimeout(() => vs.init(), 100);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });

        setTimeout(() => {
            const content = document.querySelector('#content');
            const thumbnailContainer = document.querySelector('#thumbnail-container');
            if (content && thumbnailContainer && !vs.initialized) {
                vs.init();
            }
        }, 2000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initScript);
    } else {
        initScript();
    }
})();