Greasy Fork

Greasy Fork is available in English.

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

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

您需要先安装一款用户脚本管理器扩展,例如 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();
    }

})();