小红书悬停滚动自动打开关闭 & 自动翻页

上下悬停滚动,悬停笔记自动打开,自动左右翻页,移出自动关闭

// ==UserScript==
// @name         小红书悬停滚动自动打开关闭 & 自动翻页
// @namespace    http://tampermonkey.net/
// @version      0.0.3
// @description  上下悬停滚动,悬停笔记自动打开,自动左右翻页,移出自动关闭
// @author       Qiwei
// @match        *://www.xiaohongshu.com/*
// @icon         https://raw.githubusercontent.com/qiwei-ma/xhs-hover-autoview/main/logo.png
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license      MIT
// @homepage     https://github.com/qiwei-ma/xhs-hover-autoview
// @homepageURL  https://github.com/qiwei-ma/xhs-hover-autoview
// @supportURL   https://github.com/qiwei-ma/xhs-hover-autoview/issues
// ==/UserScript==

(function () {
    'use strict';


    /** 默认值 **/
    const DEFAULTS = {
        DELAY_OPEN_MS: 800,
        DELAY_CLOSE_MS: 0,
        MOUSE_MOVE_THRESHOLD: 35,
        EDGE_THRESHOLD: 120,
        CLICK_INTERVAL_MS: 1000,
        HOVER_SCROLL_RATIO: 0.2,// 上下区域占比(0~1)
        HOVER_SCROLL_SPEED: 60,// 滚动速度(像素/秒)
        AUTO_PAGE_TURN: true,
    };

    /** 通用注册(不可用)函数 **/
    function setupConfig(name, key, minValue = 0) {
        GM_registerMenuCommand(`${name}`, () => {
            const current = Number(GM_getValue(key, DEFAULTS[key]));
            const input = prompt(`请输入 ${name}(当前值:${current})`, current);
            const newVal = parseFloat(input);
            if (input !== null && !isNaN(newVal) && newVal >= minValue) {
                GM_setValue(key, newVal);
                alert(`设置成功,请刷新页面后生效!`);
            } else {
                alert("无效输入,请输入一个不小于 " + minValue + " 的整数。");
            }
        });
    }

    // 注册(不可用)所有可配置项
    setupConfig("悬停笔记打开延迟(毫秒)", "DELAY_OPEN_MS");
    setupConfig("笔记关闭延迟(毫秒)", "DELAY_CLOSE_MS");
    setupConfig("退出滚动后鼠标移动触发阈值(像素),避免结束滚动后立即打开所在笔记", "MOUSE_MOVE_THRESHOLD", 1);
    setupConfig("图片翻页边缘触发距离(像素)", "EDGE_THRESHOLD", 10);
    setupConfig("图片自动翻页点击间隔(毫秒)", "CLICK_INTERVAL_MS", 100);
    setupConfig("悬停滚动区域页面上下占比(0-1),0.2即20%", "HOVER_SCROLL_RATIO", 0);
    setupConfig("悬停页面滚动速度(像素/秒)", "HOVER_SCROLL_SPEED", 1);


    // 添加自动翻页开关选项
    GM_registerMenuCommand("是否开启图片自动翻页(若为否则鼠标两侧轻微移动翻页)", () => {
        const current = GM_getValue("AUTO_PAGE_TURN", DEFAULTS.AUTO_PAGE_TURN);
        const newVal = !current;
        GM_setValue("AUTO_PAGE_TURN", newVal);
        alert(`自动翻页已${newVal ? '开启' : '关闭'},请刷新页面后生效!`);
    });

    /** 工具函数 **/
    const getVal = (key) => Number(GM_getValue(key, DEFAULTS[key]));

    /** 状态变量 **/
    const hoverTimers = new WeakMap();
    const openedNotes = new WeakSet();

    let isBlocked = false;
    let blockTimeout = null;
    let blockInitialPos = null;
    let onMouseMoveAfterBlock = null;

    let lastMousePos = { x: 0, y: 0 };
    window.addEventListener('mousemove', e => {
        lastMousePos = { x: e.clientX, y: e.clientY };
    });

    /** 自动打开逻辑 **/
    function triggerNoteOpen(note) {
        if (!note || openedNotes.has(note)) return;

        if (hoverTimers.has(note)) {
            clearTimeout(hoverTimers.get(note));
            hoverTimers.delete(note);
        }

        const delayTimer = setTimeout(() => {
            const cover = note.querySelector('a.cover');
            if (cover && cover.href && cover.offsetParent !== null) {
                console.log('[XHS自动打开]', cover.href);
                cover.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                openedNotes.add(note);
                setupAutoClose();
            }
            hoverTimers.delete(note);
        }, getVal('DELAY_OPEN_MS'));

        hoverTimers.set(note, delayTimer);
    }

    function tryOpenNoteUnderMouse() {
        const el = document.elementFromPoint(lastMousePos.x, lastMousePos.y);
        const note = el?.closest('.note-item');
        if (note) triggerNoteOpen(note);
    }

    function monitorMouseAfterScroll() {
        blockInitialPos = null;
        onMouseMoveAfterBlock = (event) => {
            if (!blockInitialPos) {
                blockInitialPos = { x: event.clientX, y: event.clientY };
                return;
            }
            const dx = event.clientX - blockInitialPos.x;
            const dy = event.clientY - blockInitialPos.y;
            if (Math.sqrt(dx * dx + dy * dy) >= getVal('MOUSE_MOVE_THRESHOLD')) {
                isBlocked = false;
                // console.log('[XHS自动打开] 鼠标移动超过阈值,解除滚动阻塞');
                window.removeEventListener('mousemove', onMouseMoveAfterBlock);
                tryOpenNoteUnderMouse();
            }
        };
        window.addEventListener('mousemove', onMouseMoveAfterBlock);
    }

    window.addEventListener('scroll', () => {
        if (!isBlocked) {
            isBlocked = true;
            // console.log('[XHS自动打开] 滚动开始,阻塞自动打开');
        }

        if (onMouseMoveAfterBlock) {
            window.removeEventListener('mousemove', onMouseMoveAfterBlock);
            onMouseMoveAfterBlock = null;
            blockInitialPos = null;
        }

        clearTimeout(blockTimeout);
        blockTimeout = setTimeout(() => {
            // console.log('[XHS自动打开] 滚动结束,等待鼠标移动超过阈值解除阻塞');
            monitorMouseAfterScroll();
        }, 100);
    });
    // 追踪当前正在监听的 note,避免重复绑定
    let currentHoverNote = null;
    let staticCheckTimer = null;
    let staticCheckMoveListener = null;
    let staticStartPos = null;
    let staticRetryTimer = null;

    document.addEventListener('mouseover', (e) => {
        if (isBlocked) return;

        const note = e.target.closest('.note-item');
        if (!note) return;

        // 如果是新 note,取消之前的监听
        if (note !== currentHoverNote) {
            cleanupStaticDetection();
            currentHoverNote = note;
        }

        const delay = getVal('DELAY_OPEN_MS');
        if (hoverTimers.has(note)) {
            clearTimeout(hoverTimers.get(note));
        }

        const delayTimer = setTimeout(() => {
            startStaticDetection(note);
        }, delay);

        hoverTimers.set(note, delayTimer);
    });

    document.addEventListener('mouseout', (e) => {
        const note = e.target.closest('.note-item');
        if (note && note === currentHoverNote) {
            cleanupStaticDetection();
        }
    });

    // 启动静止检测逻辑
    function startStaticDetection(note) {
        staticStartPos = { ...lastMousePos };
        let still = true;

        staticCheckMoveListener = (event) => {
            const dx = event.clientX - staticStartPos.x;
            const dy = event.clientY - staticStartPos.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist > 3) {
                still = false;
                staticStartPos = { x: event.clientX, y: event.clientY };
            } else {
                still = true;
            }
        };

        window.addEventListener('mousemove', staticCheckMoveListener);

        const tryOpenIfStatic = () => {
            const elNow = document.elementFromPoint(lastMousePos.x, lastMousePos.y);
            const currentNote = elNow?.closest('.note-item');

            if (still && currentNote === note) {
                const cover = note.querySelector('a.cover');
                if (cover && cover.href && cover.offsetParent !== null) {
                    //                     console.log('[XHS自动打开] 鼠标静止且仍在笔记上,点击打开:', cover.href);
                    cover.dispatchEvent(new MouseEvent('click', {
                        bubbles: true,
                        cancelable: true
                    }));
                    openedNotes.add(note);
                    setupAutoClose();
                    cleanupStaticDetection();
                    return;
                }
            }

            // 如果未静止或鼠标移出,继续等待 200ms 后再次检测
            staticRetryTimer = setTimeout(tryOpenIfStatic, 200);
        };

        staticCheckTimer = setTimeout(tryOpenIfStatic, 200);
    }

    // 清理所有与静止检测相关的监听器和定时器
    function cleanupStaticDetection() {
        if (staticCheckMoveListener) {
            window.removeEventListener('mousemove', staticCheckMoveListener);
            staticCheckMoveListener = null;
        }
        if (staticCheckTimer) {
            clearTimeout(staticCheckTimer);
            staticCheckTimer = null;
        }
        if (staticRetryTimer) {
            clearTimeout(staticRetryTimer);
            staticRetryTimer = null;
        }

        if (currentHoverNote) {
            if (hoverTimers.has(currentHoverNote)) {
                clearTimeout(hoverTimers.get(currentHoverNote));
                hoverTimers.delete(currentHoverNote);
            }
        }

        currentHoverNote = null;
    }


    document.addEventListener('mouseout', e => {
        const note = e.target.closest('.note-item');
        if (note && hoverTimers.has(note)) {
            clearTimeout(hoverTimers.get(note));
            hoverTimers.delete(note);
            //             console.log('[XHS自动打开] 鼠标移出,取消打开');
        }
    });

    /** 自动关闭逻辑 **/
    let closeTimeout = null;
    function setupAutoClose() {
        const noteContainer = document.getElementById('noteContainer');
        if (!noteContainer) {
            //             console.log('[XHS自动关闭] 未找到 noteContainer,稍后重试');
            setTimeout(setupAutoClose, 500);
            return;
        }

        noteContainer.onmouseleave = () => {
            clearTimeout(closeTimeout);
            closeTimeout = setTimeout(() => {
                const closeBtn = document.querySelector('.close-circle .close');
                if (closeBtn) {
                    //                     console.log('[XHS自动关闭] 鼠标移出,点击关闭');
                    closeBtn.click();
                    handleCloseBlock();
                }
            }, getVal('DELAY_CLOSE_MS'));
        };

        noteContainer.onmouseenter = () => {
            clearTimeout(closeTimeout);
            closeTimeout = null;
        };

    }

    function handleCloseBlock() {
        if (isBlocked) return;
        isBlocked = true;
        const blockStartTime = Date.now();
        const MIN_BLOCK_DURATION = 300;

        if (onMouseMoveAfterBlock) window.removeEventListener('mousemove', onMouseMoveAfterBlock);
        blockInitialPos = null;

        onMouseMoveAfterBlock = (event) => {
            if (!blockInitialPos) {
                blockInitialPos = { x: event.clientX, y: event.clientY };
                return;
            }
            const dx = event.clientX - blockInitialPos.x;
            const dy = event.clientY - blockInitialPos.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const duration = Date.now() - blockStartTime;

            if (dist >= getVal('MOUSE_MOVE_THRESHOLD') && duration >= MIN_BLOCK_DURATION) {
                isBlocked = false;
                window.removeEventListener('mousemove', onMouseMoveAfterBlock);
                tryOpenNoteUnderMouse();
            }
        };
        window.addEventListener('mousemove', onMouseMoveAfterBlock);
    }


    // === 图片左右边缘自动翻页功能(自动模式) ===
    (function setupAutoSlideNavigation() {
        let autoDirection = null;
        let lastMouseX = 0;
        let lastMouseY = 0;
        let mouseX = 0;
        let mouseY = 0;
        let lastMoveTime = 0;

        function getActiveImageContainer() {
            return document.querySelector('.swiper-slide-active .note-slider-img, .swiper-slide-active .live-photo-contain');
        }

        function clickArrowIfAvailable(selector) {
            const btnWrapper = document.querySelector(selector);
            const arrowController = btnWrapper?.closest('.arrow-controller');
            if (btnWrapper && arrowController && !arrowController.classList.contains('forbidden')) {
                btnWrapper.click();
                return true;
            }
            return false;
        }

        document.addEventListener('mousemove', (e) => {
            lastMouseX = mouseX;
            lastMouseY = mouseY;
            mouseX = e.clientX;
            mouseY = e.clientY;
            lastMoveTime = Date.now();
        });

        document.addEventListener('mouseleave', () => {
            autoDirection = null;
        });

        // === 自动翻页模式:较慢频率 ===
        setInterval(() => {
            const isAutoPageTurn = GM_getValue("AUTO_PAGE_TURN", DEFAULTS.AUTO_PAGE_TURN) === true;
            if (!isAutoPageTurn) return;

            const container = getActiveImageContainer();
            if (!container) return;

            const rect = container.getBoundingClientRect();

            const insideImage =
                  mouseX >= rect.left &&
                  mouseX <= rect.right &&
                  mouseY >= rect.top &&
                  mouseY <= rect.bottom;

            if (!insideImage) {
                autoDirection = null;
                return;
            }

            if (mouseX - rect.left < getVal('EDGE_THRESHOLD')) {
                autoDirection = 'left';
            } else if (rect.right - mouseX < getVal('EDGE_THRESHOLD')) {
                autoDirection = 'right';
            } else {
                autoDirection = null;
            }

            if (autoDirection === 'left') {
                if (clickArrowIfAvailable('.arrow-controller.left .btn-wrapper')) {
                }
            } else if (autoDirection === 'right') {
                if (clickArrowIfAvailable('.arrow-controller.right .btn-wrapper')) {
                }
            }
        }, getVal('CLICK_INTERVAL_MS')); // 自动模式使用可配置间隔

        // === 手动轻微移动模式:固定 200ms 检查 ===
        const MOVE_INTERVAL_MS = 0;

        setInterval(() => {
            const isAutoPageTurn = GM_getValue("AUTO_PAGE_TURN", DEFAULTS.AUTO_PAGE_TURN) === true;
            if (isAutoPageTurn) return;

            const now = Date.now();
            const timeSinceLastMove = now - lastMoveTime;

            if (timeSinceLastMove < MOVE_INTERVAL_MS) return;

            const container = getActiveImageContainer();
            if (!container) return;

            const rect = container.getBoundingClientRect();

            const insideImage =
                  mouseX >= rect.left &&
                  mouseX <= rect.right &&
                  mouseY >= rect.top &&
                  mouseY <= rect.bottom;

            if (!insideImage) {
                autoDirection = null;
                return;
            }

            if (mouseX === lastMouseX) return;
            lastMouseX = mouseX;
            lastMoveTime = now;

            if (mouseX - rect.left < getVal('EDGE_THRESHOLD')) {
                autoDirection = 'left';
            } else if (rect.right - mouseX < getVal('EDGE_THRESHOLD')) {
                autoDirection = 'right';
            } else {
                autoDirection = null;
            }

            if (autoDirection === 'left') {
                clickArrowIfAvailable('.arrow-controller.left .btn-wrapper');
            } else if (autoDirection === 'right') {
                clickArrowIfAvailable('.arrow-controller.right .btn-wrapper');
            }
        }, MOVE_INTERVAL_MS); // 轻微移动触发固定 200ms 检查
    })();


    // ===页面上下区域悬停自动上下滚动===
    (function setupVerticalHoverScroll() {
        let scrollContainer = null;

        function updateScrollContainer() {
            const newContainer =
                document.querySelector('.feeds-page') ||
                document.querySelector('.feeds-tab-container');

            if (newContainer !== scrollContainer) {
                scrollContainer = newContainer;
                console.log('[悬停滚动] scrollContainer 已更新:', scrollContainer);
            }
        }

        // 初始化一次
        updateScrollContainer();

        // 监听 DOM 变化
        const observer = new MutationObserver(() => {
            updateScrollContainer();
        });

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

        const sidebarList = document.querySelector('.side-bar');
        const effectiveScrollElement = document.scrollingElement;

        if (!scrollContainer || !sidebarList || !effectiveScrollElement) {
            return;
        }

        const hoverZoneRatio = GM_getValue("HOVER_SCROLL_RATIO", DEFAULTS.HOVER_SCROLL_RATIO); // 比如 0.15
        const scrollSpeed = GM_getValue("HOVER_SCROLL_SPEED", DEFAULTS.HOVER_SCROLL_SPEED); // 比如 20

        let scrollDirection = null;
        let scrollTimer = null;
        let lastScrollTime = 0;


        document.addEventListener("mousemove", (e) => {

            const feedRect = scrollContainer.getBoundingClientRect();
            const sidebarRect = sidebarList.getBoundingClientRect();

            // 计算 hover 区域矩形:从右下角向上延伸 sidebar 高度,向左宽度为 feedRect.width
            const zoneLeft = sidebarRect.right;
            const zoneRight = zoneLeft + feedRect.width;
            const zoneTop = sidebarRect.top;
            const zoneBottom = sidebarRect.bottom;

            const y = e.clientY;
            const x = e.clientX;

            // 判断是否在这个 hover 区域内
            const insideHoverZone = x >= zoneLeft && x <= zoneRight && y >= zoneTop && y <= zoneBottom;

            if (!insideHoverZone) {
                if (scrollDirection !== null) {
                }
                scrollDirection = null;
            } else {
                const zoneHeight = zoneBottom - zoneTop;
                const threshold = zoneHeight * hoverZoneRatio;

                if (y < zoneTop + threshold) {
                    scrollDirection = "up";
                } else if (y > zoneBottom - threshold) {
                    scrollDirection = "down";
                } else {
                    if (scrollDirection !== null) {
                    }
                    scrollDirection = null;
                }
            }

            // 开始或停止滚动
            if (scrollDirection && !scrollTimer) {
                scrollTimer = setInterval(() => {
                    const now = Date.now();
                    if (now - lastScrollTime < 16) return;
                    lastScrollTime = now;

                    const delta = scrollDirection === "up" ? -scrollSpeed / 10 : scrollSpeed / 10;
                    effectiveScrollElement.scrollBy(0, delta);

                    // 派发滚轮事件(可选)
                    const wheelEvent = new WheelEvent('wheel', {
                        deltaY: delta * 3,
                        bubbles: true,
                        cancelable: true
                    });
                    scrollContainer.dispatchEvent(wheelEvent);

                }, 16);
            } else if (!scrollDirection && scrollTimer) {
                clearInterval(scrollTimer);
                scrollTimer = null;
            }

        });

        document.addEventListener("mouseleave", () => {
            if (scrollTimer) {
                clearInterval(scrollTimer);
                scrollTimer = null;
            }
        });

    })();


})();
长期地址
遇到问题?请前往 GitHub 提 Issues。