nhentai Vertical Scroll [Fixed]

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Userscripts installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey installieren.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

(Ich habe bereits einen Benutzerstil Verwaltung, ich möchte ihn installieren!)

// ==UserScript==
// @name         nhentai Vertical Scroll [Fixed]
// @namespace    https://nhentai.net
// @version      1.5
// @author       JoyArz + xTsuSaKu
// @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
// @license MIT
// ==/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;
            this.extension = "webp"
        }

        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}.${this.extension}`;
        }

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

            this.extractGalleryInfo();

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

            if(document.querySelector("#bigcontainer img").src.includes("webp")){
                this.extension = "webp"
            }

            if(document.querySelector("#bigcontainer img").src.includes("jpg")){
                this.extension = "jpg"
            }

            if(document.querySelector("#bigcontainer img").src.includes("jpeg")){
                this.extension = "jpeg"
            }

            if(document.querySelector("#bigcontainer img").src.includes("png")){
                this.extension = "png"
            }

            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();
    }
})();