E-Hentai Auto-Scroll

It provides an infinite scroll feature or automatic page turning mechanism, eliminating the need for manual clicking to advance to the next page of a gallery. This allows users to view content more smoothly and continuously.

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

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

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

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

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

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         E-Hentai Auto-Scroll
// @namespace    eht-infinite-scroller
// @version      1.1
// @description  It provides an infinite scroll feature or automatic page turning mechanism, eliminating the need for manual clicking to advance to the next page of a gallery. This allows users to view content more smoothly and continuously.
// @match        https://e-hentai.org/s/*/*
// @match        https://exhentai.org/s/*/*
// @grant        GM_xmlhttpRequest
// @connect      *
// @run-at       document-end
// @license CC BY-NC-ND 4.0
// ==/UserScript==

(function () {
    'use strict';

    const INITIAL_LOAD_COUNT = 50;      // 第一次加载50张
    const LOAD_BATCH_SIZE = 50;         // 每次加载50张
    const SCROLL_THRESHOLD = 500;       // 滚动阈值
    const PRELOAD_THRESHOLD = 20;       // 预加载阈值:剩余20张时开始加载
    const MAX_CANVAS_HEIGHT = 4000;     // 单个canvas最大高度,超过则创建新canvas

    let loadedImages = [];
    let loading = false;
    let hasMorePages = true;
    let nextPageUrl = document.querySelector('a#next')?.href;
    let currentLoadCount = 0;
    let totalImageCount = 0; // 记录相册总图片数量(从页面解析)
    let lastLoadedPageNum = 0; // 记录最后加载的页码,防止重复加载

    // 分块存储canvas和对应图片
    let canvasChunks = [];
    
    // 固定canvas宽度
    let fixedCanvasWidth = 0;

    if (!nextPageUrl) {
        console.warn("[E-Hentai Infinite Scroller] 无下一页");
        return;
    }

    console.log("[E-Hentai Infinite Scroller] 初始化...");

    // 从页面解析总图片数量
    const pageInfoMatch = document.querySelector('#i6 .gpc')?.textContent?.match(/Showing (\d+)-(\d+) of (\d+)/);
    if (pageInfoMatch) {
        totalImageCount = parseInt(pageInfoMatch[3]);
        console.log(`[E-Hentai Infinite Scroller] 检测到相册总图片数: ${totalImageCount}`);
    }

    // 创建容器
    let container = document.createElement('div');
    container.id = 'long-image-container';
    container.style.margin = '20px 0';
    container.style.textAlign = 'center';

    // 替换原图区域
    const originalContainer = document.getElementById('i3');
    if (originalContainer) {
        originalContainer.style.display = 'none';
        originalContainer.parentNode.insertBefore(container, originalContainer.nextSibling);
    } else {
        document.body.appendChild(container);
    }

    // 初始化固定canvas宽度
    function initializeCanvasWidth() {
        const maxWidth = window.innerWidth * 0.9; // 最大宽度为屏幕90%
        const minWidth = 640;
        fixedCanvasWidth = Math.min(maxWidth, Math.max(minWidth, 1280));
    }

    // 设置canvas宽度
    initializeCanvasWidth();

    // 创建新的canvas块
    function createNewCanvasChunk() {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.style.display = 'block';
        canvas.style.maxWidth = '100%';
        canvas.style.margin = '0 auto';
        canvas.style.border = '1px solid #ccc';
        canvas.style.imageRendering = 'pixelated';
        
        canvas.width = fixedCanvasWidth;
        canvas.height = 0; // 初始高度为0
        
        container.appendChild(canvas);
        
        const chunk = {
            canvas: canvas,
            ctx: ctx,
            images: [],
            startY: 0
        };
        
        canvasChunks.push(chunk);
        return chunk;
    }

    // 获取当前活跃的canvas块
    function getCurrentCanvasChunk() {
        if (canvasChunks.length === 0) {
            return createNewCanvasChunk();
        }
        
        const lastChunk = canvasChunks[canvasChunks.length - 1];
        if (lastChunk.canvas.height >= MAX_CANVAS_HEIGHT) {
            return createNewCanvasChunk();
        }
        
        return lastChunk;
    }

    // 加载图片函数
    function loadImagesBatch(count) {
        // 检查是否应该继续加载
        if (loading || !hasMorePages || currentLoadCount >= count) {
            console.log(`[E-Hentai Infinite Scroller] 加载条件检查: loading=${loading}, hasMorePages=${hasMorePages}, currentLoadCount=${currentLoadCount}, count=${count}, totalImageCount=${totalImageCount}`);
            if (!hasMorePages) {
                console.log("✅ 已到达最后一页,停止加载");
            }
            if (totalImageCount > 0 && loadedImages.length >= totalImageCount) {
                console.log("✅ 已加载所有图片,停止加载");
                hasMorePages = false;
            }
            return;
        }
        
        loading = true;
        console.log(`[E-Hentai Infinite Scroller] 开始加载批次: ${count}`);

        let batchLoaded = 0;
        const loadNext = () => {
            // 检查是否应该停止加载
            if (!hasMorePages || !nextPageUrl || batchLoaded >= LOAD_BATCH_SIZE || currentLoadCount >= count) {
                loading = false;
                console.log(`[E-Hentai Infinite Scroller] 批次加载完成: batchLoaded=${batchLoaded}, hasMorePages=${hasMorePages}, currentLoadCount=${currentLoadCount}, count=${count}`);
                return;
            }

            const currentUrl = nextPageUrl;
            GM_xmlhttpRequest({
                method: 'GET',
                url: currentUrl,
                withCredentials: true,
                headers: { 'Referer': 'https://e-hentai.org/' },
                onload: function (res) {
                    const html = res.responseText;
                    const imgMatch = html.match(/<img[^>]+id=["']img["'][^>]+src=["']([^"']+)["']/);
                    const nextMatch = html.match(/<a[^>]+id=["']next["'][^>]+href=["']([^"']+)["']/);

                    if (imgMatch) {
                        const imageUrl = imgMatch[1];
                        const pageNum = parseInt(currentUrl.split('-')[2], 10);

                        // 检查是否重复加载同一页(防止重复)
                        if (lastLoadedPageNum === pageNum) {
                            console.log(`[E-Hentai Infinite Scroller] 检测到重复加载页码 ${pageNum},停止加载`);
                            hasMorePages = false;
                            loading = false;
                            return;
                        }
                        lastLoadedPageNum = pageNum;

                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: imageUrl,
                            withCredentials: true,
                            headers: { 'Referer': currentUrl },
                            responseType: 'blob',
                            onload: function (imgRes) {
                                const blob = imgRes.response;
                                const img = new Image();
                                img.onload = () => {
                                    // 再次检查是否应该停止加载(防止重复加载最后一页)
                                    if (!hasMorePages) {
                                        console.log(`[E-Hentai Infinite Scroller] 已到达最后一页,停止加载图片 ${pageNum}`);
                                        batchLoaded++;
                                        currentLoadCount++;
                                        loadNext();
                                        return;
                                    }

                                    // 检查是否已加载所有图片(基于总图片数)
                                    if (totalImageCount > 0 && loadedImages.length >= totalImageCount) {
                                        console.log(`[E-Hentai Infinite Scroller] 已加载所有 ${totalImageCount} 张图片,停止加载`);
                                        hasMorePages = false;
                                        batchLoaded++;
                                        currentLoadCount++;
                                        loadNext();
                                        return;
                                    }

                                    // 计算基于固定canvas宽度的缩放高度
                                    const scaleX = fixedCanvasWidth / img.width;
                                    const scaledHeight = img.height * scaleX;
                                    
                                    const imageInfo = {
                                        pageNum: pageNum,
                                        image: img,
                                        width: img.width,
                                        height: img.height,
                                        scaledHeight: scaledHeight,
                                        y: 0 // 相对于当前块的y位置
                                    };
                                    
                                    loadedImages.push(imageInfo);

                                    // 获取当前活跃的canvas块
                                    const currentChunk = getCurrentCanvasChunk();
                                    
                                    // 将图片添加到当前块
                                    currentChunk.images.push(imageInfo);
                                    
                                    // 计算新的y位置
                                    let totalHeightInChunk = 0;
                                    currentChunk.images.forEach(imgInChunk => {
                                        imgInChunk.y = totalHeightInChunk;
                                        totalHeightInChunk += imgInChunk.scaledHeight;
                                    });
                                    
                                    // 更新canvas高度
                                    currentChunk.canvas.height = totalHeightInChunk;

                                    // 重绘当前块
                                    redrawCanvasChunk(currentChunk);

                                    batchLoaded++;
                                    currentLoadCount++;
                                    console.log(`✅ 图片 ${pageNum} 已加载并绘制 (当前共${loadedImages.length}张)`);
                                    
                                    // 检查是否已达到总图片数
                                    if (totalImageCount > 0 && loadedImages.length >= totalImageCount) {
                                        console.log(`✅ 已加载所有 ${totalImageCount} 张图片,设置 hasMorePages = false`);
                                        hasMorePages = false;
                                    }
                                    
                                    loadNext();
                                };
                                img.src = URL.createObjectURL(blob);
                            },
                            onerror: function () {
                                console.error(`❌ 图片 ${pageNum} 加载失败`);
                                batchLoaded++;
                                currentLoadCount++;
                                loadNext();
                            }
                        });
                    } else {
                        console.warn("未找到图片");
                        batchLoaded++;
                        currentLoadCount++;
                        loadNext();
                    }

                    // 更新下一页URL和状态
                    nextPageUrl = nextMatch ? nextMatch[1] : null;
                    if (!nextPageUrl) {
                        hasMorePages = false;
                        console.log("✅ 已到达最后一页,设置 hasMorePages = false");
                    }
                },
                onerror: function () {
                    console.error("页面请求失败");
                    batchLoaded++;
                    currentLoadCount++;
                    loadNext();
                }
            });
        };

        loadNext();
    }

    // 重绘指定的canvas块
    function redrawCanvasChunk(chunk) {
        if (chunk.images.length === 0) return;

        // 清空并重新绘制
        chunk.ctx.clearRect(0, 0, fixedCanvasWidth, chunk.canvas.height);

        // 按顺序绘制每张图片
        chunk.images.forEach(img => {
            chunk.ctx.drawImage(
                img.image,
                0,
                img.y,
                fixedCanvasWidth,    // 使用固定宽度
                img.scaledHeight     // 按比例缩放的高度
            );
        });
    }

    // 滚动事件监听 - 智能预加载
    function checkScroll() {
        if (!hasMorePages) {
            console.log("[E-Hentai Infinite Scroller] 已到达最后一页,停止滚动监听");
            return;
        }

        // 额外检查:如果已加载图片数达到总图片数,停止加载
        if (totalImageCount > 0 && loadedImages.length >= totalImageCount) {
            console.log(`[E-Hentai Infinite Scroller] 已加载所有 ${totalImageCount} 张图片,停止预加载`);
            hasMorePages = false;
            return;
        }

        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        const windowHeight = window.innerHeight;
        const documentHeight = document.documentElement.scrollHeight;

        // 计算当前可视区域底部位置
        const scrollBottom = scrollTop + windowHeight;

        // 计算剩余未加载的图片数量
        if (loadedImages.length > 0) {
            // 获取最后几张图片的平均高度来估算剩余距离
            const recentImages = loadedImages.slice(-PRELOAD_THRESHOLD);
            if (recentImages.length > 0) {
                const avgHeight = recentImages.reduce((sum, img) => sum + img.scaledHeight, 0) / recentImages.length;
                const estimatedRemainingHeight = avgHeight * PRELOAD_THRESHOLD;

                // 如果距离底部小于预估的20张图片高度,开始加载
                if (documentHeight - scrollBottom < estimatedRemainingHeight) {
                    // 额外检查:如果已经加载的图片数量等于或超过总图片数量,则不加载
                    if (totalImageCount > 0 && loadedImages.length >= totalImageCount) {
                        console.log("[E-Hentai Infinite Scroller] 已加载所有图片,跳过预加载");
                        hasMorePages = false;
                        return;
                    }
                    
                    console.log(`智能预加载:当前位置${scrollBottom}, 总高度${documentHeight}, 预估剩余${estimatedRemainingHeight}`);
                    loadImagesBatch(currentLoadCount + LOAD_BATCH_SIZE);
                    return;
                }
            }
        }

        // 备用方案:如果无法估算,使用原始的触底检测
        if (documentHeight - scrollBottom < SCROLL_THRESHOLD && hasMorePages) {
            loadImagesBatch(currentLoadCount + LOAD_BATCH_SIZE);
        }
    }

    // 监听事件
    window.addEventListener('scroll', checkScroll);
    window.addEventListener('resize', () => {
        // 窗口大小改变时重新计算固定宽度
        initializeCanvasWidth();
        
        // 重新创建所有canvas块(因为宽度变了)
        container.innerHTML = '';
        canvasChunks = [];
        loadedImages.forEach(img => {
            const scaleX = fixedCanvasWidth / img.width;
            img.scaledHeight = img.height * scaleX;
        });
        
        // 重新绘制所有图片
        loadedImages.forEach(img => {
            const currentChunk = getCurrentCanvasChunk();
            currentChunk.images.push(img);
            
            // 重新计算y位置
            let totalHeightInChunk = 0;
            currentChunk.images.forEach(imgInChunk => {
                imgInChunk.y = totalHeightInChunk;
                totalHeightInChunk += imgInChunk.scaledHeight;
            });
            
            currentChunk.canvas.height = totalHeightInChunk;
            redrawCanvasChunk(currentChunk);
        });
    });

    // 初始化
    loadImagesBatch(INITIAL_LOAD_COUNT);
})();