墨水屏电纸书优化

针对墨水屏的优化 适合电纸书省电 看漫画 ①可消除移动动画(松手才移动页面) ②可设置双击放大复原 ③可屏蔽长按图片弹出的菜单 ②可设置长按放大单图 ④可给底部增加半屏空白

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         墨水屏电纸书优化
// @namespace    cc.cxuan.books
// @version      1.39
// @description  针对墨水屏的优化 适合电纸书省电 看漫画 ①可消除移动动画(松手才移动页面) ②可设置双击放大复原 ③可屏蔽长按图片弹出的菜单 ②可设置长按放大单图 ④可给底部增加半屏空白
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @noframes
// @license      MIT
// @author       cxuan.cc
// ==/UserScript==
(function(){
    // 设置项
    let my = GM_getValue('multiplierY', 2.5);
    let mx = 2.5;
    let intervalSec = GM_getValue('interval', 0.3);
    let doubleClickZoom = GM_getValue('doubleClickZoom', 2);
    let enableMx = GM_getValue('enableMx', false);
    let enableParent = GM_getValue('enableParent', false);
    let blockContextMenu = GM_getValue('blockContextMenu', false);
    let blockContextZoom = GM_getValue('blockContextZoom', 0); //状态0:关闭 1:再长按拖到左端 2:再长按拖到左上角
    let addBottomSpace = GM_getValue('addBottomSpace', false);

    function updateSettings({key, value}) {
        // 更新本地变量
        switch (key) {
            case 'multiplierY': my = value; break;
            case 'interval':
                intervalSec = value;
                stopTimer(); startTimer();
                break;
            case 'doubleClickZoom': doubleClickZoom = value; applyViewport(); break;
            case 'enableMx': enableMx = value; break;
            case 'enableParent': enableParent = value; break;
            case 'blockContextMenu': blockContextMenu = value; applyContextMenuBlock(); break;
            case 'blockContextZoom': blockContextZoom = value; applyContextMenuZoom(); break;
            case 'addBottomSpace': addBottomSpace = value; applyAddBottomSpace(); break;
        }
    }

    GM_registerMenuCommand(`设置Y轴移动倍率(之前 ${my})`, ()=>{
        let v = parseFloat(prompt('Y 轴滑动倍率:', my));
        if (!isNaN(v)) {
            GM_setValue('multiplierY', v);
            updateSettings({key:'multiplierY', value:v});
            alert(`设置Y轴移动倍率(当前 ${v})`);
        }
    });
    GM_registerMenuCommand(`设置更新间隔(之前 ${intervalSec}s)`, ()=>{
        let v = parseFloat(prompt('更新间隔(秒,0=仅松手时刷新):', intervalSec));
        if (!isNaN(v)) {
            GM_setValue('interval', v);
            updateSettings({key:'interval', value:v});
            alert(`设置更新间隔(当前 ${v}s)`);
        }
    });
    GM_registerMenuCommand(`设置双击放大倍率(之前 ${doubleClickZoom})`, ()=>{
        let v = parseFloat(prompt('双击放大倍率(0=无双击放大):', doubleClickZoom));
        if (!isNaN(v)) {
            GM_setValue('doubleClickZoom', v);
            updateSettings({key:'doubleClickZoom', value:v});
            alert(`设置双击放大倍率(当前 ${v})`);
        }
    });
    GM_registerMenuCommand(`切换x轴移动优化开关 固定${mx}倍(之前 ${enableMx ? '开' : '关'})`, ()=>{
        let v = !enableMx;
        GM_setValue('enableMx', v);
        updateSettings({key:'enableMx', value:v});
        alert(`切换x轴移动优化(当前 ${v ? '开' : '关'})`);
    });
    GM_registerMenuCommand(`切换父元素一同滚动(之前 ${enableParent ? '开' : '关'})`, ()=>{
        let v = !enableParent;
        GM_setValue('enableParent', v);
        updateSettings({key:'enableParent', value:v});
        alert(`切换父元素一同滚动(当前 ${v ? '开' : '关'})`);
    });
    GM_registerMenuCommand(`切换右键菜单屏蔽(之前 ${blockContextMenu ? '开' : '关'})`, ()=>{
        let v = !blockContextMenu;
        GM_setValue('blockContextMenu', v);
        updateSettings({key:'blockContextMenu', value:v});
        alert(`切换右键菜单屏蔽(当前 ${v ? '开' : '关'})`);
    });
    GM_registerMenuCommand(`切换长按图片放大(当前 ${(blockContextZoom==0)?'关':((blockContextZoom==1)?'再长按跳转左端':'再长按跳转左上角')})`, ()=>{
        let v = (blockContextZoom + 1) % 3;
        GM_setValue('blockContextZoom', v);
        updateSettings({key:'blockContextZoom', value:v});
        alert(`切换长按图片放大(当前 ${(v==0)?'关':((v==1)?'再长按跳转左端':'再长按跳转左上角')})`);
    });
    GM_registerMenuCommand(`切换底部增加空白(之前 ${addBottomSpace ? '开' : '关'})`, ()=>{
        let v = !addBottomSpace;
        GM_setValue('addBottomSpace', v);
        updateSettings({key:'addBottomSpace', value:v});
        alert(`切换底部增加空白(当前 ${v ? '开' : '关'})`);
    });

    //开启或禁止双指缩放
    let meta = document.querySelector('meta[name=viewport]');
    const vp = 'width=device-width, initial-scale=1, minimum-scale=0.25, maximum-scale=10, user-scalable=yes';

    const applyViewport = ()=>{
        let head = document.head || document.getElementsByTagName('head')[0] || document.documentElement;
        if(doubleClickZoom > 0){
            if (meta) {
                meta.setAttribute('content', vp);
            } else {
                meta = document.createElement('meta');
                meta.name = 'viewport';
                meta.content = vp;
                document.head.appendChild(meta);
            }
        }else{
            if (meta) {
                meta.setAttribute('content', vp);
            }
        }
        head.prepend(meta);
    };
    applyViewport();

    // 右键放大函数
    let zimg;
    let showZoomedImg = function(img, event) {
        if (zimg) {
            zimg.remove();
            zimg = null;
        }

        zimg = img.cloneNode(true);
        zimg.classList.add("ci_books_img_big");

        zimg.style.position = 'fixed';
        zimg.style.zIndex = 999999999998;
        zimg.style.pointerEvents = 'auto';
        zimg.style.maxWidth = 'none';
        zimg.style.maxHeight = 'none';

        let rect = img.getBoundingClientRect();
        let w = rect.width * doubleClickZoom;
        let h = rect.height * doubleClickZoom;
        zimg.style.width = w + 'px';
        zimg.style.height = h + 'px';

        // 计算点击点相对图片的比例
        let clickX = event.clientX - rect.left;
        let clickY = event.clientY - rect.top;

        let centerX = window.innerWidth / 2;
        let centerY = window.innerHeight / 2;

        // 让点击点移动到屏幕中心
        let left = centerX - (clickX * (w / rect.width));
        let top = centerY - (clickY * (h / rect.height));

        zimg.style.left = left + 'px';
        zimg.style.top = top + 'px';
        zimg.style.boxShadow = '0 0 20px #0009';
        zimg.style.background = '#fff';

        // 单击移除
        zimg.addEventListener('click', function(){
            zimg.remove();
            zimg = null;
        });

        // 长按逻辑
        zimg.addEventListener('contextmenu', function(e){
            if (!zimg) return;
            let w = parseInt(zimg.style.width);
            let h = parseInt(zimg.style.height);

            if (blockContextZoom === 1) {
                // 拖到最左端
                zimg.style.left = '0px';
            } else if (blockContextZoom === 2) {
                // 拖到左上角
                zimg.style.left = '0px';
                zimg.style.top = '0px';
            }
            if (!blockContextMenu) return;
            if (!e.target.closest('a')) e.preventDefault();
        },true);

        zoomedImgLimit()
        document.body.appendChild(zimg);
    }

    let zoomedImgLimit = function(isY) {
        // 检查边缘
        let left = parseInt(zimg.style.left);
        let top = parseInt(zimg.style.top);
        let w = parseInt(zimg.style.width);
        let h = parseInt(zimg.style.height);
        if (w >= window.innerWidth) {
            if (left > 0) left = 0;
            if (left + w < window.innerWidth) left = window.innerWidth - w;
        }
        if (h >= window.innerHeight) {
            if (top > 0) top = 0;
            if (top + h < window.innerHeight) top = window.innerHeight - h;
        }
        zimg.style.left = left + 'px';
        if(isY) zimg.style.top = top + 'px';
    }

    let pressTimer = null;
    // 右键菜单屏蔽函数
    let contextMenuHandler = function(e) {
        if (!blockContextMenu) return;
        if (blockContextZoom !== 0 && e.target.tagName == 'IMG') return;
        if (!e.target.closest('a')) e.preventDefault();
    }
    function applyContextMenuBlock() {
        document.removeEventListener('contextmenu', contextMenuHandler, true);
        if (blockContextMenu) {
            document.addEventListener('contextmenu', contextMenuHandler, true);
        }
    }
    applyContextMenuBlock();

    // 右键放大图片
    let contextZoomHandler = function(e) {
        // 状态0关闭
        if(blockContextZoom === 0) return;
        // 要求,只处理新图
        if (e.target.tagName == 'IMG') {
            if (zimg == e.target) {
                return;
            }
            showZoomedImg(e.target, e);
        }
        if (!blockContextMenu) return;
        if (!e.target.closest('a')) e.preventDefault();
    }
    let mouseupHandler = ()=>{
        if (pressTimer) clearTimeout(pressTimer);
    }

    function applyContextMenuZoom() {
        document.removeEventListener('contextmenu', contextZoomHandler, true);
        document.removeEventListener('mouseup', mouseupHandler, true);

        if (blockContextZoom !== 0) {
            document.addEventListener('contextmenu', contextZoomHandler, true);
            document.addEventListener('mouseup', mouseupHandler, true);
        }
    }
    applyContextMenuZoom();

    //底部增加空白
    function applyAddBottomSpace() {
        let id = 'ci-bottom-space-extension';
        if (addBottomSpace) {
            if (!document.getElementById(id)) {
                let div = document.createElement('div');
                div.id = id;
                div.style.height = '50vh';
                div.style.width = '100%';
                div.style.pointerEvents = 'none';
                document.body.appendChild(div);
            }
        } else {
            let oldDiv = document.getElementById(id);
            if (oldDiv) oldDiv.remove();
        }
    }
    applyAddBottomSpace();

    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(zimg){
                if (dY) dY = dY / my;
                let left = parseInt(zimg.style.left);
                let top = parseInt(zimg.style.top);
                if (dX) zimg.style.left = left - dX * doubleClickZoom + 'px';
                if (dY) zimg.style.top = top - dY * doubleClickZoom + 'px';
                zoomedImgLimit()
            }
            if (enableParent) {
                let node = info.el;
                while (node) {
                    if(zimg == node) continue;
                    if (dX) node.scrollLeft += dX;
                    if (dY) node.scrollTop += dY;
                    node = node.parentNode;
                }
            } else {
                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(!enableMx){
                if (Math.abs(info.cx - info.sx) > Math.abs(info.cy - info.sy) * 2){
                    doPrevent = false;
                }
            }
        }
        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;
    let isDoubleClickZoomBig = false;
    document.addEventListener('touchend', e=>{
        if (doubleClickZoom == 0) return;
        // 仅单指
        if (e.touches.length === 0 && e.changedTouches.length === 1) {
            let now = Date.now();
            if (now - lastTap < 300) {
                // 修改或插入 viewport
                let meta = document.querySelector('meta[name=viewport]');
                isDoubleClickZoomBig = !isDoubleClickZoomBig;
                const vp = `width=device-width, initial-scale=${isDoubleClickZoomBig ? doubleClickZoom : 1}, maximum-scale=10`;
                if (meta) {
                    meta.setAttribute('content', vp);
                } else {
                    meta = document.createElement('meta');
                    meta.name = 'viewport';
                    meta.content = vp;
                    document.head.appendChild(meta);
                }
            }
            lastTap = now;
        }
    }, { passive:true });

})();