墨水屏电纸书划动优化

消除划动动画 可设置划动倍率和更新间隔 可设置双指放大双击复原

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

// ==UserScript==
// @name         墨水屏电纸书划动优化
// @namespace    cc.cxuan.books
// @version      1.28
// @description  消除划动动画  可设置划动倍率和更新间隔 可设置双指放大双击复原
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @noframes
// @license      MIT
// @author       cxuan.cc
// ==/UserScript==
(function(){
    // 检查是否是双击复位后的重载,若是则恢复滚动位置并微调宽度
    const RESET_FLAG = '__mzpx_reset', POS_KEY = '__mzpx_pos';
    const docEl = document.scrollingElement || document.documentElement;
    if (sessionStorage.getItem(RESET_FLAG)) {
        // 增加 1px 宽度,确保缩放后能横向滚动
        document.documentElement.style.width = (window.innerWidth + 1) + 'px'
    }

    // 原有设置项及功能
    let mx = GM_getValue('multiplierX', 5);
    let my = GM_getValue('multiplierY', 2);
    let intervalSec = GM_getValue('interval', 0);
    let enableDoubleReset = GM_getValue('enableDoubleReset', true);

    GM_registerMenuCommand(`设置X轴移动倍率(当前 ${mx})`, ()=>{
        let v = parseFloat(prompt('X 轴滑动倍率:', mx));
        if (!isNaN(v)) {
            GM_setValue('multiplierX', mx = v);
            location.reload();
        }
    });
    GM_registerMenuCommand(`设置Y轴移动倍率(当前 ${my})`, ()=>{
        let v = parseFloat(prompt('Y 轴滑动倍率:', my));
        if (!isNaN(v)) {
            GM_setValue('multiplierY', my = v);
            location.reload();
        }
    });
    GM_registerMenuCommand(`设置更新间隔(当前 ${intervalSec}s)`, ()=>{
        let v = parseFloat(prompt('更新间隔(秒,0=仅松手时刷新):', intervalSec));
        if (!isNaN(v)) {
            GM_setValue('interval', intervalSec = v);
            location.reload();
        }
    });
    GM_registerMenuCommand(`切换双击复位(当前 ${enableDoubleReset ? '开' : '关'})`, ()=>{
        enableDoubleReset = !enableDoubleReset;
        GM_setValue('enableDoubleReset', enableDoubleReset);
        location.reload();
    });

    const initialZoom = parseFloat(document.body.style.zoom) || 1;
    const touchMap = {}; // id -> { sx, sy, cx, cy, el, lastDx, lastDy }
    let timerId = null;

    function periodicUpdate() {
        for (let id in touchMap) {
            const info = touchMap[id];
            if (info.cx == null || info.cy == null) continue;
            let totalDx = (info.sx - info.cx) * mx;
            let totalDy = (info.sy - info.cy) * my;
            let dX = totalDx - (info.lastDx || 0);
            let dY = totalDy - (info.lastDy || 0);
            if (dX) info.el.scrollLeft += dX;
            if (dY) info.el.scrollTop  += dY;
            info.lastDx = totalDx;
            info.lastDy = totalDy;
        }
    }
    function startTimer(){
        if (intervalSec > 0 && !timerId) {
            timerId = setInterval(periodicUpdate, intervalSec * 1000);
        }
    }
    function stopTimer(){
        if (timerId) {
            clearInterval(timerId);
            timerId = null;
        }
    }

    document.addEventListener('touchstart', e=>{
        if (e.touches.length > 1) return;
        for (const t of e.changedTouches) {
            let el = t.target;
            while (el && el !== document) {
                const style = getComputedStyle(el);
                const canScrollY = el.scrollHeight > el.clientHeight && /auto|scroll/.test(style.overflowY);
                const canScrollX = el.scrollWidth  > el.clientWidth  && /auto|scroll/.test(style.overflowX);
                if (canScrollY || canScrollX) break;
                el = el.parentNode;
            }
            if (!el || el === document) {
                el = document.scrollingElement || document.documentElement;
            }
            touchMap[t.identifier] = {
                sx: t.clientX, sy: t.clientY,
                cx: t.clientX, cy: t.clientY,
                el,
                lastDx: 0, lastDy: 0
            };
        }
        startTimer();
    }, { passive:false });

    document.addEventListener('touchmove', e=>{
        if (e.touches.length > 1) return;
        let doPrevent = false;
        for (const t of e.changedTouches) {
            const info = touchMap[t.identifier];
            if (!info) continue;
            info.cx = t.clientX;
            info.cy = t.clientY;
            // 允许顶部下拉刷新
            if (!(info.el === (document.scrollingElement || document.documentElement)
                  && info.el.scrollTop === 0
                  && (t.clientY - info.sy) > 0)) {
                doPrevent = true;
            }
        }
        if (doPrevent) e.preventDefault();
    }, { passive:false });

    function finishTouch(e) {
        if (e.touches.length > 1) return;
        for (const t of e.changedTouches) {
            const info = touchMap[t.identifier];
            if (!info) continue;
            if (intervalSec === 0) {
                let dx = (info.sx - t.clientX) * mx;
                let dy = (info.sy - t.clientY) * my;
                info.el.scrollLeft += dx;
                info.el.scrollTop  += dy;
            } else {
                info.cx = t.clientX;
                info.cy = t.clientY;
                periodicUpdate();
            }
            delete touchMap[t.identifier];
        }
        if (Object.keys(touchMap).length === 0) stopTimer();
    }
    document.addEventListener('touchend', finishTouch,   { passive:false });
    document.addEventListener('touchcancel', finishTouch, { passive:false });

    // 用 viewport 实现双击复位
    let lastTap = 0;
    document.addEventListener('touchend', e=>{
        if (!enableDoubleReset) return;
        // 仅单指
        if (e.touches.length === 0 && e.changedTouches.length === 1) {
            let now = Date.now();
            if (now - lastTap < 300) {
                // 记录当前位置
                let lx = docEl.scrollLeft, ly = docEl.scrollTop;
                try {
                    sessionStorage.setItem(POS_KEY, JSON.stringify({x:lx,y:ly}));
                    sessionStorage.setItem(RESET_FLAG, '1');
                } catch(err){}
                // 修改或插入 viewport
                let meta = document.querySelector('meta[name=viewport]');
                const vp = 'width=device-width, initial-scale=1, maximum-scale=1';
                if (meta) {
                    meta.setAttribute('content', vp);
                } else {
                    meta = document.createElement('meta');
                    meta.name    = 'viewport';
                    meta.content = vp;
                    document.head.appendChild(meta);
                }
                // 强制重载
                location.reload();
            }
            lastTap = now;
        }
    }, { passive:true });

})();