Greasy Fork

Greasy Fork is available in English.

页面滚动神器 (手机/PC通用版)

自动滚动页面,支持亚像素级平滑滚动,非线性速度控制。旗舰版功能:1.自动记忆位置/速度/折叠状态;2.智能防冲突(用户操作自动暂停);3.闲置自动透明。

当前为 2025-12-12 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         页面滚动神器 (手机/PC通用版)
// @namespace    https://github.com/xkzm123
// @version      1.3
// @description  自动滚动页面,支持亚像素级平滑滚动,非线性速度控制。旗舰版功能:1.自动记忆位置/速度/折叠状态;2.智能防冲突(用户操作自动暂停);3.闲置自动透明。
// @author       xkzm
// @match        *://*/*
// @grant        none
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // 防止重复加载
    if (window.suyinScrollLoaded) return;
    window.suyinScrollLoaded = true;

    // --- 核心状态 ---
    let state = {
        isScrolling: false,
        isPausedByUser: false, // 标记是否因用户操作而暂时暂停
        userInterventionTimer: null, // 用户操作计时器
        speed: 50,
        sliderValue: 22,
        lastTime: 0,
        pixelAccumulator: 0,
        isMinimized: false, // 是否最小化
        requestId: null,
        drag: { isDragging: false, startX: 0, startY: 0, initialLeft: 0, initialTop: 0, hasMoved: false },
        transparencyTimer: null // 透明度计时器
    };

    // --- 1. 记忆功能配置 (增强版:包含折叠状态) ---
    const CONFIG_KEY = 'suyin_scroll_config_v2'; // 升级Key避免旧数据冲突

    // 读取配置
    function loadConfig() {
        try {
            const raw = localStorage.getItem(CONFIG_KEY);
            if (!raw) return null;
            const config = JSON.parse(raw);

            // 简单的边界检查,防止悬浮窗跑出屏幕外
            const viewportW = window.innerWidth;
            const viewportH = window.innerHeight;

            if (config.left > viewportW - 50 || config.top > viewportH - 50 || config.left < -50 || config.top < -50) {
                return null;
            }
            return config;
        } catch (e) {
            console.error('读取滚动配置失败:', e);
            return null;
        }
    }

    // 保存配置 (包含 isMinimized)
    function saveConfig(host) {
        if (!host) host = document.getElementById('suyin-scroll-host');
        if (!host) return;

        const rect = host.getBoundingClientRect();
        const config = {
            top: rect.top,
            left: rect.left,
            sliderValue: state.sliderValue,
            isMinimized: state.isMinimized // 新增:保存折叠状态
        };
        localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
    }

    // --- 样式定义 ---
    const css = `
        :host {
            all: initial;
            font-family: system-ui, -apple-system, sans-serif;
            z-index: 2147483647;
            position: fixed;
            top: 100px;
            right: 20px;
        }
        #panel-container {
            width: 180px;
            background: rgba(20, 20, 20, 0.9);
            backdrop-filter: blur(8px);
            color: #fff;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.4);
            border: 1px solid rgba(255,255,255,0.15);
            /* 平滑过渡:宽度、高度、透明度 */
            transition: width 0.2s, height 0.2s, opacity 0.3s ease-in-out;
            user-select: none;
            -webkit-user-select: none;
            overflow: hidden;
            touch-action: none;
            opacity: 1;
        }
        /* 2. 自动透明模式样式 */
        #panel-container.idle-mode {
            opacity: 0.3;
        }
        /* 鼠标悬停或触摸时强制不透明 */
        #panel-container:hover, #panel-container:active {
            opacity: 1 !important;
        }

        /* 最小化样式 */
        #panel-container.minimized {
            width: 48px;
            height: 48px;
            border-radius: 50%;
            cursor: move;
            background: rgba(76, 175, 80, 0.9);
        }
        #panel-container.minimized:active { transform: scale(0.95); }
        #panel-container.minimized .panel-content,
        #panel-container.minimized .panel-header { display: none; }
        #panel-container.minimized .minimized-icon { display: flex; }

        .panel-header {
            padding: 12px 15px;
            background: rgba(255,255,255,0.08);
            cursor: move;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid rgba(255,255,255,0.1);
        }
        .panel-title { font-size: 13px; font-weight: 600; color: #ddd; }
        .minimize-btn {
            cursor: pointer;
            width: 22px; height: 22px;
            line-height: 20px; text-align: center;
            border-radius: 4px;
            background: rgba(255,255,255,0.1);
            font-size: 16px; transition: 0.2s;
        }
        .minimize-btn:hover { background: #ff9800; color: #000; }

        .panel-content { padding: 15px; display: flex; flex-direction: column; gap: 12px; }

        .speed-control { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #aaa; }
        .speed-val { font-family: 'Menlo', monospace; color: #4CAF50; font-weight: bold; font-size: 14px; }

        input[type=range] {
            width: 100%; height: 5px;
            background: rgba(255,255,255,0.2);
            border-radius: 3px; appearance: none; outline: none; cursor: pointer;
        }
        input[type=range]::-webkit-slider-thumb {
            appearance: none; width: 16px; height: 16px;
            border-radius: 50%; background: #4CAF50;
            box-shadow: 0 0 5px rgba(0,0,0,0.5);
            transition: transform 0.1s;
        }
        input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.2); }

        button {
            width: 100%; padding: 10px 0; border: none; border-radius: 8px;
            background: #4CAF50; color: white; font-weight: bold; font-size: 14px;
            cursor: pointer; transition: background 0.2s;
        }
        button:hover { filter: brightness(1.1); }
        button.scrolling { background: #f44336; }

        .minimized-icon {
            display: none; width: 100%; height: 100%;
            align-items: center; justify-content: center;
            font-size: 24px; color: #fff;
        }
        .hint { font-size:10px; color:#666; text-align:center; margin-top:0px; }
    `;

    // --- 逻辑函数 ---
    function calculateSpeed(val) {
        const maxSpeed = 500;
        const percentage = val / 100;
        let rawSpeed = maxSpeed * Math.pow(percentage, 2.5);
        if (val > 0 && rawSpeed < 1) rawSpeed = 1;
        if (val === 0) rawSpeed = 0;
        return Math.floor(rawSpeed);
    }

    function animationLoop(timestamp) {
        if (!state.isScrolling) return;

        // --- 3. 智能防冲突逻辑 ---
        if (state.isPausedByUser) {
            state.lastTime = timestamp;
            state.requestId = requestAnimationFrame(animationLoop);
            return;
        }

        if (!state.lastTime) state.lastTime = timestamp;
        const deltaTime = timestamp - state.lastTime;
        state.lastTime = timestamp;

        state.pixelAccumulator += (state.speed * deltaTime) / 1000;
        const pixelsToScroll = Math.trunc(state.pixelAccumulator);

        if (pixelsToScroll !== 0) {
            window.scrollBy(0, pixelsToScroll);
            state.pixelAccumulator -= pixelsToScroll;
        }

        if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 1) {
            toggleScrolling(false);
            return;
        }
        state.requestId = requestAnimationFrame(animationLoop);
    }

    function toggleScrolling(forceState, shadowRoot) {
        if (!shadowRoot) {
             const host = document.getElementById('suyin-scroll-host');
             if(host) shadowRoot = host.shadowRoot;
        }
        if (!shadowRoot) return;

        if (typeof forceState !== 'undefined') state.isScrolling = forceState;
        else state.isScrolling = !state.isScrolling;

        const btn = shadowRoot.getElementById('toggle-scroll-btn');
        if (btn) {
            if (state.isScrolling) {
                btn.textContent = '停止滚动';
                btn.classList.add('scrolling');
                state.lastTime = 0;
                state.pixelAccumulator = 0;
                state.isPausedByUser = false;
                state.requestId = requestAnimationFrame(animationLoop);
            } else {
                btn.textContent = '开始滚动';
                btn.classList.remove('scrolling');
                if (state.requestId) cancelAnimationFrame(state.requestId);
                state.requestId = null;
            }
        }
    }

    // --- 3. 智能防冲突:监听用户行为 ---
    function initSmartPause() {
        const handleUserInteraction = () => {
            if (!state.isScrolling) return;

            state.isPausedByUser = true;

            if (state.userInterventionTimer) {
                clearTimeout(state.userInterventionTimer);
            }

            state.userInterventionTimer = setTimeout(() => {
                state.isPausedByUser = false;
                state.lastTime = 0;
            }, 1000);
        };

        const opts = { passive: true };
        window.addEventListener('wheel', handleUserInteraction, opts);
        window.addEventListener('touchmove', handleUserInteraction, opts);
        window.addEventListener('keydown', (e) => {
            if(['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Space'].includes(e.code)) {
                handleUserInteraction();
            }
        }, opts);
    }

    // --- UI 构建 ---
    function createUI() {
        const host = document.createElement('div');
        host.id = 'suyin-scroll-host';
        document.body.appendChild(host);

        // --- 1. 记忆功能:应用保存的配置 ---
        const savedConfig = loadConfig();
        if (savedConfig) {
            host.style.top = `${savedConfig.top}px`;
            host.style.left = `${savedConfig.left}px`;
            host.style.right = 'auto';
            state.sliderValue = savedConfig.sliderValue;
            state.isMinimized = !!savedConfig.isMinimized; // 恢复状态
        }

        const shadow = host.attachShadow({ mode: 'open' });

        const styleTag = document.createElement('style');
        styleTag.textContent = css;
        shadow.appendChild(styleTag);

        const container = document.createElement('div');
        container.id = 'panel-container';

        // 如果记忆了最小化状态,初始化时直接加上 class
        if (state.isMinimized) {
            container.classList.add('minimized');
        }

        state.speed = calculateSpeed(state.sliderValue);

        container.innerHTML = `
            <div class="minimized-icon">⬇</div>
            <div class="panel-header">
                <span class="panel-title">平滑滚动</span>
                <span class="minimize-btn" title="最小化">−</span>
            </div>
            <div class="panel-content">
                <div class="speed-control">
                    <span>速度</span>
                    <span class="speed-val">${state.speed} px/s</span>
                </div>
                <input type="range" id="speed-slider" min="0" max="100" value="${state.sliderValue}">
                <button id="toggle-scroll-btn">开始滚动</button>
                <div class="hint">Alt+Z 开始 / Alt+X 停止</div>
            </div>
        `;
        shadow.appendChild(container);

        const slider = shadow.getElementById('speed-slider');
        const speedDisplay = container.querySelector('.speed-val');
        const btn = shadow.getElementById('toggle-scroll-btn');
        const minBtn = container.querySelector('.minimize-btn');

        // --- 2. 自动透明功能逻辑 ---
        const wakeUpPanel = () => {
            container.classList.remove('idle-mode');
            if (state.transparencyTimer) clearTimeout(state.transparencyTimer);

            state.transparencyTimer = setTimeout(() => {
                if (!state.drag.isDragging) {
                    container.classList.add('idle-mode');
                }
            }, 3000);
        };

        container.addEventListener('mouseenter', wakeUpPanel);
        container.addEventListener('touchstart', wakeUpPanel);
        container.addEventListener('mousemove', wakeUpPanel);
        wakeUpPanel();


        slider.addEventListener('input', (e) => {
            state.sliderValue = parseInt(e.target.value);
            state.speed = calculateSpeed(state.sliderValue);
            speedDisplay.textContent = `${state.speed} px/s`;
            wakeUpPanel();
        });

        slider.addEventListener('change', () => {
             saveConfig(host);
        });

        slider.addEventListener('touchstart', (e) => { e.stopPropagation(); wakeUpPanel(); });
        slider.addEventListener('mousedown', (e) => { e.stopPropagation(); wakeUpPanel(); });

        btn.addEventListener('click', () => {
            toggleScrolling(undefined, shadow);
            wakeUpPanel();
        });

        // 最小化按钮逻辑 (新增:保存状态)
        minBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            state.isMinimized = true;
            container.classList.add('minimized');
            wakeUpPanel();
            saveConfig(host); // 保存最小化状态
        });

        initDrag(host, container);
        initSmartPause();

        document.addEventListener('keydown', (e) => {
            if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return;
            if (e.altKey && e.code === 'KeyZ') toggleScrolling(true, shadow);
            else if (e.altKey && e.code === 'KeyX') toggleScrolling(false, shadow);
        });
    }

    // --- 拖拽逻辑 ---
    function initDrag(host, container) {

        function getClientCoords(e) {
            if (e.touches && e.touches.length > 0) {
                return { x: e.touches[0].clientX, y: e.touches[0].clientY };
            }
            return { x: e.clientX, y: e.clientY };
        }

        function onStart(e) {
            container.classList.remove('idle-mode');
            if (state.transparencyTimer) clearTimeout(state.transparencyTimer);

            const target = e.target;
            const isControl = target.closest('input') || target.closest('button');
            const isHeader = target.closest('.panel-header');

            if (!state.isMinimized && !isHeader) return;
            if (isControl) return;

            state.drag.isDragging = true;
            state.drag.hasMoved = false;

            const coords = getClientCoords(e);
            state.drag.startX = coords.x;
            state.drag.startY = coords.y;

            const rect = host.getBoundingClientRect();
            state.drag.initialLeft = rect.left;
            state.drag.initialTop = rect.top;

            host.style.right = 'auto';
            host.style.left = `${rect.left}px`;
            host.style.top = `${rect.top}px`;
            container.style.transition = 'none';

            if(e.type === 'touchstart') {
            } else {
                e.preventDefault();
            }
        }

        function onMove(e) {
            if (!state.drag.isDragging) return;
            if (e.cancelable) e.preventDefault();

            const coords = getClientCoords(e);
            const dx = coords.x - state.drag.startX;
            const dy = coords.y - state.drag.startY;

            if (Math.abs(dx) > 2 || Math.abs(dy) > 2) state.drag.hasMoved = true;

            host.style.left = `${state.drag.initialLeft + dx}px`;
            host.style.top = `${state.drag.initialTop + dy}px`;
        }

        function onEnd() {
            if (state.drag.isDragging) {
                state.drag.isDragging = false;
                container.style.transition = 'width 0.2s, height 0.2s, opacity 0.3s ease-in-out';

                saveConfig(host); // 拖拽结束,保存位置

                if (state.transparencyTimer) clearTimeout(state.transparencyTimer);
                state.transparencyTimer = setTimeout(() => {
                    container.classList.add('idle-mode');
                }, 3000);
            }
        }

        container.addEventListener('mousedown', onStart);
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onEnd);

        container.addEventListener('touchstart', onStart, { passive: false });
        document.addEventListener('touchmove', onMove, { passive: false });
        document.addEventListener('touchend', onEnd);

        // PC 点击恢复 (新增:保存状态)
        container.addEventListener('click', (e) => {
            if (state.isMinimized && !state.drag.hasMoved) {
                state.isMinimized = false;
                container.classList.remove('minimized');
                container.classList.remove('idle-mode');
                saveConfig(host); // 保存展开状态
            }
        });

        // 手机 点击恢复 (新增:保存状态)
        container.addEventListener('touchend', (e) => {
            if (state.isMinimized && !state.drag.hasMoved && !state.drag.isDragging) {
                 state.isMinimized = false;
                 container.classList.remove('minimized');
                 container.classList.remove('idle-mode');
                 saveConfig(host); // 保存展开状态
            }
        });
    }

    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', createUI);
    } else {
        createUI();
    }

})();