您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
智能检测滚动容器,丝滑平顺的自动滚动体验,支持各种网页应用
- // ==UserScript==
- // @name 元气滚动
- // @name:en Genki Scroll
- // @name:zh-CN 元气滚动
- // @namespace https://github.com/genkiscroll
- // @version 1.5.0
- // @description 智能检测滚动容器,丝滑平顺的自动滚动体验,支持各种网页应用
- // @description:en Intelligent scroll container detection with silky smooth auto-scrolling experience for all web applications
- // @description:zh-CN 智能检测滚动容器,丝滑平顺的自动滚动体验,支持各种网页应用
- // @author Genki Developer
- // @license MIT
- // @homepage https://github.com/genkiscroll/genki-scroll
- // @supportURL https://github.com/genkiscroll/genki-scroll/issues
- // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgo8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojZmY2YjZiO3N0b3Atb3BhY2l0eToxIiAvPgo8c3RvcCBvZmZzZXQ9IjUwJSIgc3R5bGU9InN0b3AtY29sb3I6IzRlY2RjNDtzdG9wLW9wYWNpdHk6MSIgLz4KPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdHlsZT0ic3RvcC1jb2xvcjojNDViN2QxO3N0b3Atb3BhY2l0eToxIiAvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+CjxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjMwIiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIvPgo8cGF0aCBkPSJNMzIgMTBBMjIgMjIgMCAwIDEgNTQgMzJBMjIgMjIgMCAwIDEgMzIgNTQiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMyIgZmlsbD0ibm9uZSIvPgo8cGF0aCBkPSJNMzIgMTZBMTYgMTYgMCAwIDAgMTYgMzJBMTYgMTYgMCAwIDAgMzIgNDgiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIvPgo8cGF0aCBkPSJNMzIgMjJBMTAgMTAgMCAwIDEgNDIgMzJBMTAgMTAgMCAwIDEgMzIgNDIiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIvPgo8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIzIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K
- // @match *://*/*
- // @exclude *://localhost/*
- // @exclude *://127.0.0.1/*
- // @exclude *://192.168.*/*
- // @exclude *://10.*/*
- // @exclude *://172.16.*/*
- // @exclude *://172.17.*/*
- // @exclude *://172.18.*/*
- // @exclude *://172.19.*/*
- // @exclude *://172.20.*/*
- // @exclude *://172.21.*/*
- // @exclude *://172.22.*/*
- // @exclude *://172.23.*/*
- // @exclude *://172.24.*/*
- // @exclude *://172.25.*/*
- // @exclude *://172.26.*/*
- // @exclude *://172.27.*/*
- // @exclude *://172.28.*/*
- // @exclude *://172.29.*/*
- // @exclude *://172.30.*/*
- // @exclude *://172.31.*/*
- // @exclude *://file:///*
- // @noframes
- // @grant none
- // @run-at document-idle
- // @compatible chrome >=60
- // @compatible firefox >=55
- // @compatible edge >=79
- // @compatible safari >=13
- // @compatible opera >=47
- // ==/UserScript==
- /*
- MIT License
- Copyright (c) 2024 Genki Scroll
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
- (function() {
- 'use strict';
- // 滚动配置
- let scrollConfig = {
- isScrolling: false,
- scrollInterval: 50, // 50毫秒间隔,更流畅
- scrollPixelsPerStep: 2, // 每次滚动2像素,更平滑
- scrollTimer: null,
- lastScrollTop: 0,
- unchangedCount: 0, // 连续无变化计数
- scrollContainer: null, // 当前滚动容器
- useAnimation: true // 是否使用动画滚动
- };
- let controlPanel = null;
- let floatingIcon = null;
- // 常见的滚动容器选择器
- const SCROLL_SELECTORS = [
- // 通用滚动容器
- '[data-scroll-container]',
- '.scroll-container',
- '.scrollable',
- '.overflow-auto',
- '.overflow-y-auto',
- '.overflow-scroll',
- '.overflow-y-scroll',
- // 飞书相关
- '.suit-doc-page',
- '.suite-doc-editor',
- '.feishu-doc-content',
- '.doc-render-container',
- '.lark-virtual-scroll',
- '.docs-texteventtarget-iframe',
- // 其他常见应用
- '.notion-page-content',
- '.roam-article',
- '.obsidian-vault',
- '.markdown-body',
- '.main-content',
- '.content-wrapper',
- '.article-content',
- '.document-content',
- // 通用内容区域
- 'main',
- 'article',
- '.main',
- '.content',
- '#content',
- '.wrapper',
- '.container'
- ];
- // 创建悬浮图标
- function createFloatingIcon() {
- const icon = document.createElement('div');
- icon.id = 'autoScrollIcon';
- // 创建螺旋图标
- icon.innerHTML = `
- <div class="spiral-icon">
- <div class="spiral-line spiral-1"></div>
- <div class="spiral-line spiral-2"></div>
- <div class="spiral-line spiral-3"></div>
- <div class="center-dot"></div>
- </div>
- `;
- icon.style.cssText = `
- position: fixed;
- top: 50%;
- right: 20px;
- width: 50px;
- height: 50px;
- background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 50%, #45b7d1 100%);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- z-index: 10000;
- box-shadow: 0 4px 15px rgba(0,0,0,0.3);
- transition: all 0.3s ease;
- user-select: none;
- transform: translateY(-50%);
- overflow: hidden;
- `;
- // 添加螺旋图标的样式
- const style = document.createElement('style');
- style.textContent = `
- .spiral-icon {
- position: relative;
- width: 30px;
- height: 30px;
- animation: gentleRotate 4s linear infinite;
- }
- .spiral-line {
- position: absolute;
- border: 2px solid rgba(255, 255, 255, 0.9);
- border-radius: 50%;
- animation: spiral-pulse 2s ease-in-out infinite;
- }
- .spiral-1 {
- width: 25px;
- height: 25px;
- top: 2.5px;
- left: 2.5px;
- border-top-color: transparent;
- border-left-color: transparent;
- animation-delay: 0s;
- }
- .spiral-2 {
- width: 18px;
- height: 18px;
- top: 6px;
- left: 6px;
- border-bottom-color: transparent;
- border-right-color: transparent;
- animation-delay: 0.3s;
- }
- .spiral-3 {
- width: 11px;
- height: 11px;
- top: 9.5px;
- left: 9.5px;
- border-top-color: transparent;
- border-left-color: transparent;
- animation-delay: 0.6s;
- }
- .center-dot {
- position: absolute;
- width: 4px;
- height: 4px;
- background: white;
- border-radius: 50%;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- animation: dot-pulse 1.5s ease-in-out infinite;
- }
- @keyframes gentleRotate {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
- }
- @keyframes spiral-pulse {
- 0%, 100% { opacity: 0.7; transform: scale(1); }
- 50% { opacity: 1; transform: scale(1.1); }
- }
- @keyframes dot-pulse {
- 0%, 100% { opacity: 0.8; transform: translate(-50%, -50%) scale(1); }
- 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.3); }
- }
- /* 滚动状态的样式 */
- .scroll-active .spiral-icon {
- animation: fastRotate 1s linear infinite;
- }
- .scroll-active .spiral-line {
- border-color: rgba(255, 255, 255, 1);
- animation: spiral-active 0.8s ease-in-out infinite;
- }
- .scroll-active .center-dot {
- animation: dot-active 0.6s ease-in-out infinite;
- }
- @keyframes fastRotate {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
- }
- @keyframes spiral-active {
- 0%, 100% { opacity: 0.9; transform: scale(1); }
- 50% { opacity: 1; transform: scale(1.2); }
- }
- @keyframes dot-active {
- 0%, 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
- 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.5); }
- }
- `;
- // 将样式添加到页面
- if (!document.getElementById('spiral-icon-styles')) {
- style.id = 'spiral-icon-styles';
- document.head.appendChild(style);
- }
- // 悬停效果
- icon.addEventListener('mouseenter', () => {
- icon.style.transform = 'translateY(-50%) scale(1.1)';
- icon.style.boxShadow = '0 6px 20px rgba(255,107,107,0.4)';
- icon.style.background = 'linear-gradient(135deg, #ff8a80 0%, #80cbc4 50%, #64b5f6 100%)';
- });
- icon.addEventListener('mouseleave', () => {
- icon.style.transform = 'translateY(-50%) scale(1)';
- icon.style.boxShadow = '0 4px 15px rgba(0,0,0,0.3)';
- icon.style.background = 'linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 50%, #45b7d1 100%)';
- });
- // 点击切换滚动状态
- icon.addEventListener('click', () => {
- if (scrollConfig.isScrolling) {
- stopAutoScroll();
- } else {
- startAutoScroll();
- }
- });
- document.body.appendChild(icon);
- return icon;
- }
- // 创建简化的状态提示
- function createStatusTooltip() {
- const tooltip = document.createElement('div');
- tooltip.id = 'scrollTooltip';
- tooltip.style.cssText = `
- position: fixed;
- top: 50%;
- right: 80px;
- background: #2c3e50;
- color: white;
- padding: 10px 15px;
- border-radius: 20px;
- font-family: 'Segoe UI', Arial, sans-serif;
- font-size: 14px;
- z-index: 10001;
- transform: translateY(-50%);
- opacity: 0;
- transition: all 0.3s ease;
- pointer-events: none;
- white-space: nowrap;
- box-shadow: 0 4px 15px rgba(0,0,0,0.3);
- `;
- tooltip.textContent = '点击开始自动滚动';
- document.body.appendChild(tooltip);
- return tooltip;
- }
- // 显示状态提示
- function showTooltip(text, duration = 2000) {
- let tooltip = document.getElementById('scrollTooltip');
- if (!tooltip) {
- tooltip = createStatusTooltip();
- }
- tooltip.textContent = text;
- tooltip.style.opacity = '1';
- if (duration > 0) {
- setTimeout(() => {
- if (tooltip) {
- tooltip.style.opacity = '0';
- }
- }, duration);
- }
- }
- // 隐藏状态提示
- function hideTooltip() {
- const tooltip = document.getElementById('scrollTooltip');
- if (tooltip) {
- tooltip.style.opacity = '0';
- }
- }
- // 智能检测滚动容器
- function detectScrollContainer() {
- // 检查是否有明显的滚动容器
- for (const selector of SCROLL_SELECTORS) {
- const elements = document.querySelectorAll(selector);
- for (const element of elements) {
- if (isScrollable(element)) {
- console.log(`🎯 检测到滚动容器: ${selector}`, element);
- return element;
- }
- }
- }
- // 检查iframe内容
- const iframes = document.querySelectorAll('iframe');
- for (const iframe of iframes) {
- try {
- const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
- if (iframeDoc && isScrollable(iframeDoc.documentElement)) {
- console.log('🎯 检测到iframe滚动容器', iframe);
- return { element: iframeDoc.documentElement, isIframe: true };
- }
- } catch (e) {
- // 跨域iframe无法访问
- }
- }
- // 自动检测页面中可滚动的元素
- const allElements = document.querySelectorAll('*');
- const candidates = [];
- for (const element of allElements) {
- if (isScrollable(element)) {
- const height = element.scrollHeight - element.clientHeight;
- if (height > 200) { // 至少有200px的滚动空间
- candidates.push({ element, height });
- }
- }
- }
- // 选择滚动空间最大的元素
- if (candidates.length > 0) {
- candidates.sort((a, b) => b.height - a.height);
- console.log('🎯 自动检测到滚动容器', candidates[0].element);
- return candidates[0].element;
- }
- // 默认使用document.documentElement
- console.log('🎯 使用默认滚动容器: document.documentElement');
- return document.documentElement;
- }
- // 检查元素是否可滚动
- function isScrollable(element) {
- if (!element) return false;
- const style = window.getComputedStyle(element);
- const overflowY = style.overflowY;
- const overflowX = style.overflowX;
- const overflow = style.overflow;
- // 检查是否设置了滚动样式
- const hasScrollStyle = ['auto', 'scroll'].includes(overflowY) ||
- ['auto', 'scroll'].includes(overflow);
- // 检查是否有实际的滚动内容
- const hasScrollContent = element.scrollHeight > element.clientHeight + 5;
- return hasScrollStyle && hasScrollContent;
- }
- // 获取容器的滚动位置
- function getScrollTop(container) {
- if (!container) return 0;
- if (container.isIframe) {
- return container.element.scrollTop || container.element.parentElement?.scrollTop || 0;
- }
- if (container === document.documentElement || container === document.body) {
- return Math.max(
- document.documentElement.scrollTop,
- document.body.scrollTop,
- window.pageYOffset || 0
- );
- }
- return container.scrollTop || 0;
- }
- // 设置容器的滚动位置
- function setScrollTop(container, value) {
- if (!container) return;
- if (container.isIframe) {
- container.element.scrollTop = value;
- return;
- }
- if (container === document.documentElement || container === document.body) {
- document.documentElement.scrollTop = value;
- document.body.scrollTop = value;
- window.scrollTo(0, value);
- return;
- }
- container.scrollTop = value;
- }
- // 平滑滚动函数
- function smoothScrollBy(container, deltaY) {
- if (!container) return;
- try {
- // 优先使用平滑滚动
- if (container === document.documentElement || container === document.body) {
- // 使用window.scrollBy的平滑滚动
- window.scrollBy({
- top: deltaY,
- behavior: 'auto' // 使用auto而不是smooth,避免冲突
- });
- } else if (container.scrollBy) {
- // 对于其他元素,使用element.scrollBy
- container.scrollBy({
- top: deltaY,
- behavior: 'auto'
- });
- } else {
- // 回退到直接设置scrollTop
- const currentScrollTop = getScrollTop(container);
- setScrollTop(container, currentScrollTop + deltaY);
- }
- } catch (e) {
- // 如果平滑滚动失败,使用传统方法
- const currentScrollTop = getScrollTop(container);
- setScrollTop(container, currentScrollTop + deltaY);
- }
- }
- // 获取页面总高度和可视高度
- function getScrollInfo(container) {
- if (!container) {
- container = document.documentElement;
- }
- const scrollTop = getScrollTop(container);
- let scrollHeight, clientHeight;
- if (container.isIframe) {
- scrollHeight = container.element.scrollHeight;
- clientHeight = container.element.clientHeight;
- } else if (container === document.documentElement || container === document.body) {
- scrollHeight = Math.max(
- document.documentElement.scrollHeight,
- document.body.scrollHeight
- );
- clientHeight = Math.max(
- document.documentElement.clientHeight,
- window.innerHeight
- );
- } else {
- scrollHeight = container.scrollHeight;
- clientHeight = container.clientHeight;
- }
- return { scrollTop, scrollHeight, clientHeight };
- }
- // 检查是否可以继续滚动
- function canScrollMore(container) {
- const { scrollTop, scrollHeight, clientHeight } = getScrollInfo(container);
- // 检查是否到达底部(留10px容差)
- const isAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 10);
- return !isAtBottom;
- }
- // 开始自动滚动
- function startAutoScroll() {
- if (scrollConfig.isScrolling) return;
- // 检测滚动容器
- scrollConfig.scrollContainer = detectScrollContainer();
- if (!scrollConfig.scrollContainer) {
- showTooltip('❌ 未找到可滚动内容', 3000);
- return;
- }
- scrollConfig.isScrolling = true;
- scrollConfig.lastScrollTop = getScrollTop(scrollConfig.scrollContainer);
- scrollConfig.unchangedCount = 0;
- // 更新图标状态
- floatingIcon.classList.add('scroll-active');
- // 显示状态
- showTooltip('正在丝滑滚动中...', 0);
- // 使用requestAnimationFrame实现超平滑滚动
- let lastTime = 0;
- const targetInterval = scrollConfig.scrollInterval; // 50ms间隔
- function smoothScrollLoop(currentTime) {
- if (!scrollConfig.isScrolling) return;
- // 控制滚动频率
- if (currentTime - lastTime >= targetInterval) {
- const beforeScroll = getScrollTop(scrollConfig.scrollContainer);
- // 检查是否可以继续滚动
- if (!canScrollMore(scrollConfig.scrollContainer)) {
- stopAutoScroll();
- showTooltip('🎉 到底啦!', 3000);
- return;
- }
- // 执行小幅度平滑滚动
- smoothScrollBy(scrollConfig.scrollContainer, scrollConfig.scrollPixelsPerStep);
- const afterScroll = getScrollTop(scrollConfig.scrollContainer);
- // 检查滚动是否有效
- if (Math.abs(afterScroll - beforeScroll) < 1) {
- scrollConfig.unchangedCount++;
- if (scrollConfig.unchangedCount >= 20) { // 连续20次无效滚动
- // 尝试重新检测滚动容器
- console.log('🔄 重新检测滚动容器...');
- scrollConfig.scrollContainer = detectScrollContainer();
- if (!canScrollMore(scrollConfig.scrollContainer)) {
- stopAutoScroll();
- showTooltip('🎉 到底啦!', 3000);
- return;
- }
- scrollConfig.unchangedCount = 0;
- }
- } else {
- scrollConfig.unchangedCount = 0;
- }
- scrollConfig.lastScrollTop = afterScroll;
- lastTime = currentTime;
- }
- // 继续动画循环
- scrollConfig.scrollTimer = requestAnimationFrame(smoothScrollLoop);
- }
- // 开始平滑滚动循环
- scrollConfig.scrollTimer = requestAnimationFrame(smoothScrollLoop);
- }
- // 停止滚动
- function stopAutoScroll() {
- scrollConfig.isScrolling = false;
- if (scrollConfig.scrollTimer) {
- cancelAnimationFrame(scrollConfig.scrollTimer);
- scrollConfig.scrollTimer = null;
- }
- // 恢复图标状态
- floatingIcon.classList.remove('scroll-active');
- // 隐藏状态提示
- hideTooltip();
- }
- // 初始化
- function init() {
- floatingIcon = createFloatingIcon();
- // 键盘快捷键
- document.addEventListener('keydown', (e) => {
- // Esc 键停止滚动
- if (e.key === 'Escape' && scrollConfig.isScrolling) {
- stopAutoScroll();
- showTooltip('滚动已停止', 2000);
- }
- // 空格键开始/停止滚动
- if (e.key === ' ' && e.target === document.body) {
- e.preventDefault();
- if (scrollConfig.isScrolling) {
- stopAutoScroll();
- showTooltip('滚动已停止', 2000);
- } else {
- startAutoScroll();
- }
- }
- });
- // 拖拽悬浮图标
- let isDragging = false;
- let dragOffset = { x: 0, y: 0 };
- let startPos = { x: 0, y: 0 };
- floatingIcon.addEventListener('mousedown', (e) => {
- isDragging = true;
- startPos.x = e.clientX;
- startPos.y = e.clientY;
- dragOffset.x = e.clientX - floatingIcon.offsetLeft;
- dragOffset.y = e.clientY - floatingIcon.offsetTop;
- floatingIcon.style.cursor = 'grabbing';
- e.preventDefault();
- });
- document.addEventListener('mousemove', (e) => {
- if (!isDragging) return;
- floatingIcon.style.left = (e.clientX - dragOffset.x) + 'px';
- floatingIcon.style.top = (e.clientY - dragOffset.y) + 'px';
- floatingIcon.style.right = 'auto';
- floatingIcon.style.transform = 'none';
- });
- document.addEventListener('mouseup', (e) => {
- if (isDragging) {
- isDragging = false;
- floatingIcon.style.cursor = 'pointer';
- // 检查是否是点击而非拖拽
- const distance = Math.sqrt(
- Math.pow(e.clientX - startPos.x, 2) +
- Math.pow(e.clientY - startPos.y, 2)
- );
- // 如果移动距离小于5px,认为是点击
- if (distance < 5) {
- // 点击事件会自动触发,这里不需要额外处理
- }
- }
- });
- // 鼠标悬停显示提示
- floatingIcon.addEventListener('mouseenter', () => {
- if (!scrollConfig.isScrolling) {
- showTooltip('点击开始丝滑滚动', 2000);
- }
- });
- floatingIcon.addEventListener('mouseleave', () => {
- if (!scrollConfig.isScrolling) {
- setTimeout(() => {
- hideTooltip();
- }, 500);
- }
- });
- // 监听页面变化,重新检测滚动容器
- const observer = new MutationObserver(() => {
- if (scrollConfig.isScrolling) {
- // 如果当前容器不再可滚动,重新检测
- if (!canScrollMore(scrollConfig.scrollContainer)) {
- const newContainer = detectScrollContainer();
- if (newContainer !== scrollConfig.scrollContainer) {
- console.log('🔄 页面变化,切换滚动容器', newContainer);
- scrollConfig.scrollContainer = newContainer;
- }
- }
- }
- });
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- attributes: true
- });
- }
- // 页面加载完成后初始化
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
- })();