网页随心滚

滚动到顶/底、滚动条-新、记录页面滚动、屏幕常亮和自动滚动

// ==UserScript==
// @name        网页随心滚
// @namespace   http://tampermonkey.net/
// @version     2.0
// @description 滚动到顶/底、滚动条-新、记录页面滚动、屏幕常亮和自动滚动
// @author      ^o^
// @match       *://*/*
// @grant       GM_registerMenuCommand
// @run-at      document-start
// ==/UserScript==

(function() {
    'use strict';
    let w = window, d = document;
    let eScrollBtn = true, eScrollBar = true, eWakeLock = true;
    let scrollBarPosition = localStorage.getItem('scrollBarPosition') || 'right'; // 'left' or 'right'
    let scrollBtnPosition = localStorage.getItem('scrollBtnPosition') || 'right'; // 'left' or 'right'

    // 注册菜单命令
    GM_registerMenuCommand('滚动到顶/底按钮:开/关', toggleScrollBtn);
    GM_registerMenuCommand('滚动条:开/关', toggleScrollBar);
    GM_registerMenuCommand('滚动条位置:左/右切换', toggleScrollBarPosition);
    GM_registerMenuCommand('滚动到顶/底按钮位置:左/右切换', toggleScrollBtnPosition);
    GM_registerMenuCommand('保持屏幕常亮:开/关', toggleWakeLock);

    function toggleScrollBtn() {
        eScrollBtn = !eScrollBtn;
        let b = d.getElementById('scroll-top-btn');
        if (b) b.remove();
        if (eScrollBtn) initScrollBtn();
    }

    function toggleScrollBar() {
        eScrollBar = !eScrollBar;
        let s = d.getElementById('theScrollBar');
        if (s) s.remove();
        if (eScrollBar) initScrollBar();
    }

    function toggleWakeLock() {
        eWakeLock = !eWakeLock;
        if (eWakeLock) requestWL();
        else if (wakeLock) wakeLock.release();
    }

    function toggleScrollBarPosition() {
        scrollBarPosition = scrollBarPosition === 'left' ? 'right' : 'left';
        localStorage.setItem('scrollBarPosition', scrollBarPosition);
        let s = d.getElementById('theScrollBar');
        if (s) {
            if (scrollBarPosition === 'right') {
                s.style.right = '2vw';
                s.style.left = 'auto';
            } else {
                s.style.left = '2vw';
                s.style.right = 'auto';
            }
        }
    }

    function toggleScrollBtnPosition() {
        scrollBtnPosition = scrollBtnPosition === 'left' ? 'right' : 'left';
        localStorage.setItem('scrollBtnPosition', scrollBtnPosition);
        let b = d.getElementById('scroll-top-btn');
        if (b) {
            b.style.left = scrollBtnPosition === 'left' ? '15px' : '';
            b.style.right = scrollBtnPosition === 'right' ? '15px' : '';
        }
    }

    function initScrollBtn() {
        let b = d.createElement('button');
        b.textContent = '▲';
        b.id = 'scroll-top-btn';
        Object.assign(b.style, {
            position: 'fixed', bottom: '15%', zIndex: 999999,
            width: '35px', height: '35px', borderRadius: '50%', padding: 0,
            background: 'rgba(255,255,255,0.3)', backdropFilter: 'blur(5px)',
            display: 'none',
            fontSize: '16px', textAlign: 'center', lineHeight: '35px',
            fontWeight: 'bold', cursor: 'pointer', transition: 'all 0.3s ease',
            border: 'none'
        });
        // 根据位置设置 left 或 right
        if (scrollBtnPosition === 'left') {
            b.style.left = '13px';
        } else {
            b.style.right = '13px';
        }
        d.body.appendChild(b);
        let lastY = w.pageYOffset, t;
        w.addEventListener('scroll', () => {
            b.textContent = w.scrollY > lastY ? '▼' : '▲';
            lastY = w.scrollY;
            b.style.display = w.pageYOffset > 100 ? 'block' : 'none';
            clearTimeout(t);
            t = setTimeout(() => b.style.display = 'none', 2000);
        });
        b.addEventListener('mouseenter', () => {
            b.style.background = 'rgba(255,255,255,0.6)';
            b.style.transform = 'scale(1.1)';
        });
        b.addEventListener('mouseleave', () => {
            b.style.background = 'rgba(255,255,255,0.3)';
            b.style.transform = 'scale(1)';
        });
        b.addEventListener('click', () => w.scrollTo({
            top: b.textContent === '▲' ? 0 : d.documentElement.scrollHeight,
            behavior: 'smooth'
        }));
    }

    function initScrollBar() {
        // 创建滚动条 DOM 元素
        const theScrollBar = document.createElement("div");

        // 设置滚动条的 ID 和文本
        theScrollBar.id = "theScrollBar";
        theScrollBar.innerHTML = "▲<br>▼";

        // 设置滚动条样式
        theScrollBar.setAttribute(
            "style",
            "font-size:2.6vw ;width:6vw ;line-height:5vw ;display: block;text-align:center ;background-color:rgba(255,255,255) ;opacity: 0 ;box-shadow:0px 1px 5px rgba(0,0,0,0.2) ;color:#000 ;position:fixed ;top: -14vw;right:-10vw ;z-index:9999999 ;transition: opacity 0.4s ease-in-out,right 0.4s; border-radius:3vw "
        );

        // 根据位置设置 left 或 right
        if (scrollBarPosition === 'left') {
            theScrollBar.style.left = '2vw';
            theScrollBar.style.right = 'auto';
        } else {
            theScrollBar.style.right = '-10vw';
            theScrollBar.style.left = 'auto';
        }

        // 将滚动条元素添加到页面中
        document.body.appendChild(theScrollBar);

        // 存储网页高度和上次滚动位置
        let webHeight = null;
        let lastScrollTop = null;

        // 更新滚动条位置的函数
        function updateScrollBar() {
            // 获取当前的滚动位置
            const scrollTop = window.scrollY;

            // 如果滚动位置改变了,重新计算滚动条位置
            if (scrollTop !== lastScrollTop) {
                const scrollBarTop =
                    (scrollTop / webHeight) * (window.innerHeight - theScrollBar.clientHeight);
                if (scrollBarTop < 0) {
                    theScrollBar.style.top = "0";
                } else if (scrollBarTop + theScrollBar.clientHeight > window.innerHeight) {
                    theScrollBar.style.top = `${window.innerHeight - theScrollBar.clientHeight}px`;
                    webHeight = document.documentElement.scrollHeight - window.innerHeight;
                } else {
                    theScrollBar.style.top = `${scrollBarTop}px`;
                }
                lastScrollTop = scrollTop;
            }

            // 使用 requestAnimationFrame 函数,优化渲染性能
            window.requestAnimationFrame(updateScrollBar);
        }

        // 添加 touchstart 事件监听器,当用户在手机上开始触摸屏幕时触发
        window.addEventListener("touchstart", function() {
            //如果网页高度过低,不需要添加滚动条
            if (document.documentElement.scrollHeight <= window.innerHeight * 2) {
                return;
            }

            // 获取网页高度,并开始更新滚动条位置
            webHeight = document.documentElement.scrollHeight - window.innerHeight;
            updateScrollBar();
        });

        // 定义一些与触摸事件相关的变量
        let startOffset = null;

        // 滚动条开始滚动时触发的函数
        function startScroll(event) {
            event.preventDefault();
            event.stopPropagation();
            startOffset = event.changedTouches[0].clientY - parseInt(theScrollBar.style.top);
        }

        // 滚动条正在滚动时触发的函数
        function scrolling(event) {
            event.preventDefault();
            event.stopPropagation();
            // 计算当前滚动条的位置和滑动距离,并更新滚动位置和滚动条位置
            const currentY = event.changedTouches[0].clientY;
            const scrollBarTop = currentY - startOffset;
            if (scrollBarTop < 0) {
                theScrollBar.style.top = "0px";
            } else if (scrollBarTop > window.innerHeight - theScrollBar.clientHeight) {
                theScrollBar.style.top = `${window.innerHeight - theScrollBar.clientHeight}px`;
            } else {
                theScrollBar.style.top = `${scrollBarTop}px`;
            }
            const scrollTop =
                (scrollBarTop / (window.innerHeight - theScrollBar.clientHeight)) * webHeight;
            window.scrollTo(window.scrollX, scrollTop);
        }

        // 为滚动条添加触摸事件监听器
        theScrollBar.addEventListener("touchstart", startScroll, { passive: false });
        theScrollBar.addEventListener("touchmove", scrolling, { passive: false });

        // 停止滚动1秒后隐藏
        let timer;
        window.addEventListener("scroll", function() {
            clearTimeout(timer);
            if (scrollBarPosition === 'right') {
                theScrollBar.style.right = "2vw";
                theScrollBar.style.left = "auto";
            } else {
                theScrollBar.style.left = "2vw";
                theScrollBar.style.right = "auto";
            }
            theScrollBar.style.opacity = "0.8";
            timer = setTimeout(() => {
                if (scrollBarPosition === 'right') {
                    theScrollBar.style.right = "-10vw";
                    theScrollBar.style.left = "auto";
                } else {
                    theScrollBar.style.left = "-10vw";
                    theScrollBar.style.right = "auto";
                }
                theScrollBar.style.opacity = "0";
            }, 1000);
        });

        // 定义触顶/底反弹动画的函数
        function bounceAnimation() {
            // 获取当前滚动条的位置
            const scrollTop = window.scrollY;

            // 获取滚动条的高度
            const scrollBarHeight = theScrollBar.clientHeight;

            // 获取滚动速度
            const scrollSpeed = Math.abs(scrollTop - lastScrollTop);

            // 设置滚动速度阈值
            const threshold = 7;

            // 如果滚动条触顶,且滚动速度超过阈值,执行反弹动画
            if (scrollTop === 0 && scrollSpeed > threshold) {
                theScrollBar.style.animation = "bounce-down 0.4s";
                setTimeout(() => {
                    theScrollBar.style.animation = "";
                }, 500);
            }

            // 如果滚动条触底,且滚动速度超过阈值,执行反弹动画
            if (scrollTop + window.innerHeight >= document.documentElement.scrollHeight && scrollSpeed > threshold) {
                theScrollBar.style.animation = "bounce-up 0.4s";
                setTimeout(() => {
                    theScrollBar.style.animation = "";
                }, 500);
            }
        }

        // 添加触顶/底反弹动画的事件监听器
        window.addEventListener("scroll", bounceAnimation);

        // CSS样式
        const styles = `
        @keyframes bounce-down {
            0% {
                transform: translateY(0);
            }
            30% {
                transform: translateY(10px);
            }
            100% {
                transform: translateY(0);
            } 
        }
        @keyframes bounce-up {
            0% {
                transform: translateY(0);
            } 
             30% {
                transform: translateY(-10px);
            }
            100% {
                transform: translateY(0);
            }
        }
        `;

        // 创建样式元素并将样式添加到头部
        const styleElement = document.createElement("style");
        styleElement.innerHTML = styles;
        document.head.appendChild(styleElement);
    }

    let wakeLock = null;
    async function requestWL() {
        if (!('wakeLock' in navigator)) return;
        try {
            wakeLock = await navigator.wakeLock.request("screen");
        } catch (e) {}
    }
    eWakeLock && requestWL();

    let scrolling = false, interval, speed = 25, pixels = 1;
    function toggleAutoScroll() {
        if (scrolling) {
            scrolling = false;
            clearInterval(interval);
        } else {
            scrolling = true;
            interval = setInterval(() => {
                w.scrollBy(0, pixels);
                if (w.innerHeight + w.scrollY >= d.body.scrollHeight) {
                    w.scrollBy(0, 1);
                }
            }, speed);
        }
    }
    GM_registerMenuCommand('自动滚动:开始/停止', toggleAutoScroll);
    GM_registerMenuCommand('自动滚动:配置参数', () => {
        let s = prompt('滚动间隔(ms):', speed);
        if (s !== null) speed = parseInt(s) || speed;
        let p = prompt('每次滚动像素(px):', pixels);
        if (p !== null) pixels = parseInt(p) || pixels;
    });

    eScrollBtn && initScrollBtn();
    eScrollBar && initScrollBar();

    // 记录页面滚动位置
    w.addEventListener('scroll', () => {
        localStorage.setItem('pageScrollPosition', JSON.stringify({
            top: w.scrollY,
            left: w.scrollX
        }));
    });

    // 页面加载时恢复滚动位置
    w.addEventListener('load', () => {
        let position = localStorage.getItem('pageScrollPosition');
        if (position) {
            let scrollPosition = JSON.parse(position);
            w.scrollTo(scrollPosition.left, scrollPosition.top);
        }
    });

})();