自动滚动页面 (最终重构版)

Shift+S 激活。

// ==UserScript==
// @name         自动滚动页面 (最终重构版)
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Shift+S 激活。
// @author       Leeyw & Cjz & Gemini
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';

    // --- 全局变量与状态 ---
    let panelElement = null, scrolling = false, scrollSpeed = 30, targetScrollY = 0, lastTimestamp = 0;
    let currentTheme = 'dark', currentOpacity = 1.0;
    const SYNC_THRESHOLD = 10;
    let zoomObserver = null, antiZoomProbe = null;

    let lastHoveredElement = document.body;
    let scrollTarget = window;

    const themes = {
        dark: { panelBg: '#282c34', panelColor: '#e0e0e0', buttonBg: '#5c6370', buttonColor: '#e0e0e0', inputBg: '#3f444f', inputColor: '#e0e0e0' },
        light: { panelBg: '#f0f0f0', panelColor: '#212121', buttonBg: '#ffffff', buttonColor: '#212121', inputBg: '#ffffff', inputColor: '#212121' }
    };

    // --- 智能滚动相关函数 ---
    function findScrollableTarget(element) {
        if (!element) return window;
        let el = element;
        while (el && el !== document.documentElement) {
            const style = window.getComputedStyle(el);
            if ((style.overflowY === 'auto' || style.overflowY === 'scroll') && el.scrollHeight > el.clientHeight + 1) {
                return el;
            }
            el = el.parentElement;
        }
        return window;
    }

    const getScroller = (elementOnDemand) => {
        const target = elementOnDemand ? findScrollableTarget(elementOnDemand) : scrollTarget;
        const isWindow = target === window;
        const scrollElement = isWindow ? (document.scrollingElement || document.documentElement) : target;

        return {
            scrollTo: (y) => {
                if (isWindow) window.scrollTo(0, y);
                else target.scrollTop = y;
            },
            getScrollTop: () => isWindow ? window.scrollY : target.scrollTop,
            scrollToTop: () => target.scrollTo({ top: 0, behavior: 'smooth' }),
            scrollToBottom: () => target.scrollTo({ top: scrollElement.scrollHeight, behavior: 'smooth' }),
        };
    };

    // --- 核心功能:创建并显示面板 ---
    async function createPanel() {
        if (panelElement) return;

        await loadSettings();

        panelElement = document.createElement('div');
        Object.assign(panelElement.style, {
            position: 'fixed', right: '10px', bottom: '10px', zIndex: '2147483647',
            padding: '5px', borderRadius: '5px', fontFamily: 'sans-serif',
            boxShadow: '0 2px 8px rgba(0,0,0,0.3)', lineHeight: '1.2', fontSize: '12px',
            transition: 'opacity 0.1s'
        });

        // Helper functions for UI creation
        const createRow = (styles = {}) => { const r = document.createElement('div'); Object.assign(r.style, { display: 'flex', alignItems: 'center', minHeight: '20px' }, styles); return r; };
        const createButton = (text, styles = {}) => { const b = document.createElement('button'); b.textContent = text; Object.assign(b.style, { border: '1px solid #888', borderRadius: '3px', cursor: 'pointer', padding: '1px 4px', fontSize: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, styles); return b; };
        const createLabel = (text) => { const l = document.createElement('span'); l.textContent = text; Object.assign(l.style, { width: '50px', textAlign: 'right', marginRight: '3px', userSelect: 'none' }); return l; };

        // 创建所有元素
        const speedRow = createRow({ marginBottom: '3px' });
        const opacityRow = createRow({ marginBottom: '3px' });
        const buttonRow = createRow({ justifyContent: 'space-between' });

        const speedSlider = document.createElement('input');
        const scrollTopButton = createButton('⬆', { width: '22px', height: '22px', padding: '0', marginLeft: 'auto' });
        const opacitySlider = document.createElement('input');
        const scrollBottomButton = createButton('⬇', { width: '22px', height: '22px', padding: '0', marginLeft: 'auto' });
        const startStopButton = createButton('开始');
        const toggleThemeButton = createButton('主题');
        const speedInput = document.createElement('input');

        // 组装行
        Object.assign(speedSlider, { type: 'range', min: '1', max: '400', step: '1', value: Math.abs(scrollSpeed) });
        speedSlider.style.width = '75px';
        speedRow.appendChild(createLabel('速度:'));
        speedRow.appendChild(speedSlider);
        speedRow.appendChild(scrollTopButton);

        Object.assign(opacitySlider, { type: 'range', min: '0.2', max: '1.0', step: '0.05', value: currentOpacity });
        opacitySlider.style.width = '75px';
        opacityRow.appendChild(createLabel('透明度:'));
        opacityRow.appendChild(opacitySlider);
        opacityRow.appendChild(scrollBottomButton);

        Object.assign(speedInput, { type: 'number', step: '1', value: scrollSpeed });
        Object.assign(speedInput.style, { width: '55px', border: '1px solid #888', borderRadius: '3px', padding: '1px 3px', textAlign: 'center', fontSize: '12px', margin: '0 0px' });
        [startStopButton, speedInput, toggleThemeButton].forEach(el => buttonRow.appendChild(el));

        [speedRow, opacityRow, buttonRow].forEach(el => panelElement.appendChild(el));
        document.body.appendChild(panelElement);

        // --- 事件监听 ---
        const allButtons = [startStopButton, toggleThemeButton, scrollTopButton, scrollBottomButton];
        speedSlider.addEventListener('input', () => {
            const newMagnitude = parseFloat(speedSlider.value);
            const sign = Math.sign(scrollSpeed) || 1;
            scrollSpeed = newMagnitude * sign;
            speedInput.value = scrollSpeed;
            saveSettings();
        });
        speedInput.addEventListener('change', () => {
            const newSpeed = parseFloat(speedInput.value) || 0;
            scrollSpeed = newSpeed;
            speedSlider.value = Math.min(400, Math.abs(newSpeed));
            saveSettings();
        });
        scrollTopButton.addEventListener('click', () => getScroller(lastHoveredElement).scrollToTop());
        scrollBottomButton.addEventListener('click', () => getScroller(lastHoveredElement).scrollToBottom());
        opacitySlider.addEventListener('input', () => {
            currentOpacity = opacitySlider.value;
            panelElement.style.opacity = currentOpacity;
            saveSettings();
        });
        startStopButton.addEventListener('click', () => {
            scrolling = !scrolling;
            startStopButton.textContent = scrolling ? '停止' : '开始';
            if (scrolling) {
                scrollTarget = findScrollableTarget(lastHoveredElement);
                lastTimestamp = 0;
                targetScrollY = getScroller().getScrollTop();
                window.requestAnimationFrame(autoScroll);
            }
        });
        toggleThemeButton.addEventListener('click', () => {
            currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
            applyTheme(currentTheme, toggleThemeButton, speedInput, allButtons);
        });

        // --- 初始化状态 ---
        panelElement.style.opacity = currentOpacity;
        const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
        currentTheme = prefersDark ? 'dark' : 'light';
        applyTheme(currentTheme, toggleThemeButton, speedInput, allButtons);
        startAntiZoom();
    }

    // --- 设置管理与辅助函数 ---
    const getSettingsKey = () => `autoScrollSettings_${window.location.hostname}`;
    async function saveSettings() { try { await GM_setValue(getSettingsKey(), { speed: scrollSpeed, opacity: currentOpacity }); } catch (e) { console.error('自动滚屏脚本保存设置失败,可能是权限不足。', e); } }
    async function loadSettings() { try { const saved = await GM_getValue(getSettingsKey(), {}); scrollSpeed = saved.speed ?? 30; currentOpacity = saved.opacity ?? 1.0; } catch (e) { console.error('自动滚屏脚本读取设置失败,将使用默认设置。', e); scrollSpeed = 30; currentOpacity = 1.0; } }
    const applyTheme = (themeName, themeBtn, speedInputRef, buttonsRef) => { const theme = themes[themeName]; Object.assign(panelElement.style, { backgroundColor: theme.panelBg, color: theme.panelColor }); themeBtn.textContent = themeName === 'dark' ? '暗' : '明'; Object.assign(speedInputRef.style, { backgroundColor: theme.inputBg, color: theme.inputColor }); buttonsRef.forEach(btn => Object.assign(btn.style, { backgroundColor: theme.buttonBg, color: theme.buttonColor })); };
    function startAntiZoom() { if (antiZoomProbe) return; antiZoomProbe = document.createElement('div'); Object.assign(antiZoomProbe.style, { position: 'fixed', top: '0', left: '0', width: '100vw', height: '0', visibility: 'hidden', zIndex: '-1' }); document.body.appendChild(antiZoomProbe); const updateZoom = () => { if (!panelElement || !antiZoomProbe) return; const probeWidth = antiZoomProbe.getBoundingClientRect().width; if (probeWidth === 0) return; const zoomFactor = window.innerWidth / probeWidth; requestAnimationFrame(() => { if (panelElement) panelElement.style.zoom = 1 / zoomFactor; }); }; zoomObserver = new ResizeObserver(updateZoom); zoomObserver.observe(antiZoomProbe); requestAnimationFrame(updateZoom); }
    function stopAntiZoom() { if (zoomObserver) zoomObserver.disconnect(); if (antiZoomProbe) antiZoomProbe.remove(); zoomObserver = null; antiZoomProbe = null; if (panelElement) panelElement.style.zoom = '1'; }

    // --- 核心滚动逻辑 (修正) ---
    function autoScroll(timestamp) {
        if (!scrolling || !panelElement) return;
        const scroller = getScroller();
        if (!lastTimestamp) { lastTimestamp = timestamp; window.requestAnimationFrame(autoScroll); return; }

        const deltaTime = (timestamp - lastTimestamp) / 1000;
        lastTimestamp = timestamp;

        const actualScrollY = scroller.getScrollTop();
        if (Math.abs(actualScrollY - targetScrollY) > SYNC_THRESHOLD) {
            targetScrollY = actualScrollY;
        }

        targetScrollY += scrollSpeed * deltaTime;
        scroller.scrollTo(targetScrollY);

        window.requestAnimationFrame(autoScroll);
    }

    const togglePanelVisibility = () => { if (!panelElement) { createPanel(); } else { const isVisible = panelElement.style.display !== 'none'; if (isVisible) { panelElement.style.display = 'none'; stopAntiZoom(); } else { panelElement.style.display = 'block'; startAntiZoom(); } } };

    // --- 全局事件监听 ---
    document.addEventListener('keydown', (e) => { if (e.shiftKey && e.key === 'S') { e.preventDefault(); togglePanelVisibility(); } });
    document.addEventListener('mousemove', e => { if (e.target !== lastHoveredElement) lastHoveredElement = e.target; });
})();