Greasy Fork is available in English.

Eink Polish Beta

优化网页与第三方浏览器插件在 eink 设备上的显示。禁用 transition、animation、box-shadow、filter。加重字体。添加边框。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Eink Polish Beta
// @namespace    http://tampermonkey.net/
// @version      3.3
// @description  优化网页与第三方浏览器插件在 eink 设备上的显示。禁用 transition、animation、box-shadow、filter。加重字体。添加边框。
// @author       chen
// @match        https://*/*
// @exclude      https://vscode.dev/*
// @exclude      https://gemini.google.com/*
// @exclude      https://chatgpt.com/*
// @exclude      https://developer.mozilla.org/*
// @run-at document-start
// ==/UserScript==

(function () {
    'use strict'; // 开启严格模式,让浏览器更规范地执行代码

    // 1. 定义我们要强制生效的 CSS 样式
    const hostname = window.location.hostname; // 比如 "www.youtube.com"
    const config = {
        "*": {
            "css": `
                div {
                    /* --- 基础清理:禁用动画和特效 --- */
                    transition-duration: 0.001s !important;     /* 禁用所有渐变过渡, 不可以设置为 none, 小红书图片翻页会出现问题 */
                    transition-delay: 0s !important;
                    animation-duration: 0s !important;      /* 禁用所有动画, Gemini 推荐不要设置为 none, 会在少数情况出现问题 */
                    animation-delay: 0s !important;
                    box-shadow: none !important;                /* 禁用所有元素阴影 */
                    filter: none !important;                    /* 禁用所有滤镜(比如模糊效果) */
                    /* --- 控件 --- */
                    // border-color: #333333 !important;           /* 让边框变成深色,防止按钮背景变白后找不到按钮在哪里 */
                }

                p, h1, h2, h3, h4, h5, h6, li {
                    font-weight: 600 !important;               /* 加粗,提高墨水屏上的辨识度 */
                    // color: #000 !important;                     /*  强制纯黑文字 */
                    // -webkit-text-stroke: 0.5px white;    /* 白色描边 */
                    // paint-order: stroke fill;          /* 白色描边不会侵占原本字体 */
                }

                span, div, a {
                    font-weight: 600 !important;
                }

            `,
            "shadowCss": `
                div {
                    /* --- 基础清理:禁用动画和特效 --- */
                    transition: none !important;     /* 禁用所有渐变过渡, 不可以设置为 none, 小红书图片翻页会出现问题 */
                    animation: none !important;      /* 禁用所有动画, Gemini 推荐不要设置为 none, 会在少数情况出现问题 */
                    box-shadow: none !important;                /* 禁用所有元素阴影 */
                    filter: none !important;                    /* 禁用所有滤镜(比如模糊效果) */
                    /* --- 控件 --- */
                    border-color: #333333 !important;           /* 让边框变成深色,防止按钮背景变白后找不到按钮在哪里 */
                }
            `
        },
        "*.substack.com": {
            "css": `
                p {
                    color: #000 !important;                     /*  强制纯黑文字 */
                }
            `
        },
        // "jetbrains.com": {
        //     "css": `
        //         p, h1, h2, h3, h4, h5, h6 {
        //             font-weight: 600 !important;               /* 加粗,提高墨水屏上的辨识度 */
        //             color: #000 !important;                     /*  强制纯黑文字 */
        //             -webkit-text-stroke: 1px white;    /* 白色描边 */
        //             paint-order: stroke fill;          /* 白色描边不会侵占原本字体 */
        //         }
        //     `
        // },
        // "*.xiaohongshu.com": {
        //     "css": `
        //         span {
        //             color: #000 !important;                     /*  强制纯黑文字 */
        //         }
        //     `
        // }
    };


    /*==== 拼接css ===*/

    // 1. 域名匹配与优先级打分函数
    function getMatchScore(hostname, ruleKey) {
        // 优先级 1:全局匹配 "*"
        if (ruleKey === "*") {
            return 1;
        }

        // 优先级最高:精确匹配 (例如 "mail.google.com" 或 "google.com")
        // 分数设为 1000,确保它永远在最后被拼接,优先级最高
        if (hostname === ruleKey) {
            return 1000;
        }

        // 优先级中等:通配符匹配 (例如 "*.google.com")
        if (ruleKey.startsWith("*.")) {
            const baseDomain = ruleKey.slice(2); // 取出 "google.com"

            // 【重要】只匹配子域名,不匹配主域名(即 *.google.com 不会匹配 google.com)
            // 这样实现了 *.google.com 和 google.com 分开处理的要求
            if (hostname.endsWith('.' + baseDomain)) {
                // 利用 baseDomain 的长度作为分数,实现通配符间的优先级
                // 比如 *.mail.google.com (分数 25) 优先级大于 *.google.com (分数 20)
                return 10 + baseDomain.length;
            }
        }

        // 0 表示不匹配
        return 0;
    }

    // ==========================================
    // 2. 整合调用:收集匹配项 -> 按优先级排序 -> 拼接
    // ==========================================
    let finalCss = config["*"]?.css || "";
    let finalShadowCss = config["*"]?.shadowCss || "";
    let matchedRules = []; // 用于存放当前域名匹配到的所有规则

    // 遍历所有的配置规则
    Object.keys(config).forEach(key => {
        const score = getMatchScore(hostname, key);

        // 如果 score > 0,说明匹配成功,存入数组
        if (score > 0) {
            matchedRules.push({
                key: key,
                score: score,
                css: config[key].css,
                shadowCss: config[key].shadowCss
            });
        }
    });

    // 按优先级升序排序 (分数小的在前面,分数大的在后面覆盖)
    matchedRules.sort((a, b) => a.score - b.score);

    // 按照排好的顺序拼接 CSS
    matchedRules.forEach(rule => {
        if (rule.css) {
            finalCss += `\n/* From ${rule.key} (Priority: ${rule.score}) */\n${rule.css}`;
        }
        if (rule.shadowCss) {
            finalShadowCss += `\n/* From ${rule.key} (Shadow, Priority: ${rule.score}) */\n${rule.shadowCss}`;
        }
    });

    // 此时 finalCss 和 finalShadowCss 已经完成了拼接,高优先级的在最底下

    // 2. 核心动作:把上面的 CSS 注入到指定的“区域”
    function injectStyle(target, css) {
        // 如果这个区域已经打过我们留下的标记,说明注入过了,直接跳过,防止重复添加
        if (target.__styleInjected) return;

        // 创建一个 <style> 标签,并把我们的 CSS 文本塞进去
        const style = document.createElement('style');
        style.textContent = css;

        // 把 <style> 标签正式插入到目标区域中
        target.appendChild(style);

        // 给这个区域打上标记,表示“我已经处理过啦”
        target.__styleInjected = true;
    }

    // 3. 扫描网页节点:寻找并处理那些带有 Shadow DOM 的元素
    // 因为你提到“不存在嵌套情况”,所以我们只需要查一次表面即可,不需要无限往下深挖
    function scanAndInject(node) {
        // 第一步:如果当前检查的这个元素自身就是一个带有 Shadow DOM 的节点,直接注入
        if (node.shadowRoot) {
            injectStyle(node.shadowRoot, finalShadowCss);
        }

        // 第二步:如果这个元素是个正常的 HTML 标签(比如 <div>),找找它肚子里有没有带 Shadow DOM 的子元素
        if (node.querySelectorAll) {
            // querySelectorAll('*') 会把当前节点下的所有子孙元素都找出来
            node.querySelectorAll('*').forEach(el => {
                if (el.shadowRoot) {
                    injectStyle(el.shadowRoot, finalShadowCss);
                }
            });
        }
    }

    // ==========================================
    // 下面是脚本的执行主流程
    // ==========================================

    // 【新增修复】原代码漏掉了最关键的一步:给网页本身(主 DOM)注入样式!
    // 我们必须先给主网页注入,这样网页大部分内容的动画和阴影才会被消除
    injectStyle(document.head || document.documentElement, finalCss);

    // 页面结构刚加载完时,进行一次全局扫描(主要是扫出已经存在的 Shadow DOM)
    window.addEventListener('DOMContentLoaded', () => {
        scanAndInject(document.body);
    });

    // 创建一个“监视器” (MutationObserver)
    // 作用:现在很多网页(比如 Substack)往下滑动时会动态加载新文章或评论
    // 这个监视器就是盯着网页,一旦有新元素加进来,立刻对新元素进行扫描和去动画处理
    const observer = new MutationObserver(mutations => {
        mutations.forEach(m => {
            m.addedNodes.forEach(newNode => {
                // nodeType === 1 代表它是一个正常的 HTML 标签元素(排除了普通的纯文本文字)
                if (newNode.nodeType === 1) {
                    scanAndInject(newNode);
                }
            });
        });
    });

    // 启动监视器,盯着整个网页 (document)
    // childList: true 监视子元素的增加或删除
    // subtree: true 不仅监视儿子,连孙子、曾孙子(所有后代)的变动也要监视
    observer.observe(document, {
        childList: true,
        subtree: true
    });

})();