您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
上下悬停滚动,悬停笔记自动打开,自动左右翻页,移出自动关闭
// ==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; } }); })(); })();