Repository: chitosai/eye_protector Branch: master Commit: b93ac34439f1 Files: 11 Total size: 24.4 KB Directory structure: gitextract_1m0dqxb_/ ├── _locales/ │ ├── en/ │ │ └── messages.json │ └── zh_CN/ │ └── messages.json ├── css/ │ └── options.css ├── js/ │ ├── main.js │ ├── option.options.js │ ├── option.popup.js │ └── utility.js ├── manifest.json ├── options.html ├── popup.html └── readme.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: _locales/en/messages.json ================================================ { "extName": { "message": "Eye Protector" }, "extDescription": { "message": "May it be the best eye-protect extension on earth." }, "strProtocolMsg": { "message": "The extension only works under http/https url." }, "strPositive": { "message": "Positive Mode" }, "strPassive": { "message": "Passive Mode" }, "strEnableForCurrentDomain": { "message": "Enable for this domain" }, "strDisableForCurrentDomain": { "message": "Disable for this domain" }, "strCurrentMode": { "message": "Running in " }, "strAbout": { "message": "About" }, "strRepository": { "message": "· Source code on Github. If you like the extension, please give me a star, thanks :)" }, "strMode": { "message": "Mode" }, "strPositiveModeDesc": { "message": "Positive

Replace background color for all domains, you can disable eye-protect for particular domains.

" }, "strPassiveModeDesc": { "message": "Passive

Not replace background color by default, you can enable eye-protect for particular domains mamually.

" }, "strGlobalSetting": { "message": "Global Settings" }, "strReplaceTextColor": { "message": "Replace text color to black(#000)" }, "strReplaceInputColor": { "message": "Replace color of INPUTs" }, "strCustomBgColor": { "message": "Custom background color" }, "strSave": { "message": "Save" }, "strRestore": { "message": "Restore to default color" }, "strSaveSuccess": { "message": "New color saved" }, "strDonate": { "message": "Much appreciate if you'd like to buy me a cup of cola :)" }, "strBrightnessThreshold": { "message": "Brightness Thershold" }, "strBrightnessThresholdTip1": { "message": "Pick a brightness threshold that fits you" }, "strBrightnessThresholdTip2": { "message": "(For most people you don't need to modify it)" } } ================================================ FILE: _locales/zh_CN/messages.json ================================================ { "extName": { "message": "保护眼睛" }, "extDescription": { "message": "阿姆斯特朗回旋喷气式阿姆斯特朗墨镜" }, "strProtocolMsg": { "message": "此插件仅在 http/https 协议的域名下生效。" }, "strPositive": { "message": "主动模式" }, "strPassive": { "message": "被动模式" }, "strEnableForCurrentDomain": { "message": "此域名启用护眼模式" }, "strDisableForCurrentDomain": { "message": "此域名关闭护眼模式" }, "strCurrentMode": { "message": "正在下运行" }, "strAbout": { "message": "关于" }, "strRepository": { "message": "· 代码托管于Github,如果喜欢这个扩展请给我一个Star,谢谢:)" }, "strMode": { "message": "工作模式" }, "strPositiveModeDesc": { "message": "主动模式

默认替换所有网页的颜色,您可以在不需要替换颜色的域名下关闭护眼功能。

" }, "strPassiveModeDesc": { "message": "被动模式

默认不替换任何网页的颜色,您需要自行设置哪些域名需要开启护眼功能。

" }, "strGlobalSetting": { "message": "全局设置" }, "strReplaceTextColor": { "message": "文字替换为黑色" }, "strReplaceInputColor": { "message": "替换输入框颜色" }, "strCustomBgColor": { "message": "自定义背景色" }, "strSave": { "message": "保存" }, "strRestore": { "message": "还原为豆沙绿" }, "strSaveSuccess": { "message": "保存成功" }, "strDonate": { "message": "如果你愿意赞助我一杯可乐就太感谢了:)" }, "strBrightnessThreshold": { "message": "亮度阈值" }, "strBrightnessThresholdTip1": { "message": "设置一个适合你的亮度阈值" }, "strBrightnessThresholdTip2": { "message": "(一般来说直接用默认值就可以了)" } } ================================================ FILE: css/options.css ================================================ body { font: 14px "PingFangSC-Light", "Segoe UI", "Microsoft YaHei"; margin: .6rem; width: 150px; } #option-page { width: 470px; margin: 1rem auto; } #option-page .option-list { margin-bottom: 1.6rem; } #col-1{ float: left; width: 300px; padding-right: 20px; } #col-2{ float: right; width: 150px; } .title { font-weight: bold; font-size: 1.5rem; margin: 1rem 0 .6rem; } .list{} .item { border-radius: 3px; cursor: pointer; line-height: 2em; padding: 0 10px 0 25px; } .item:hover { background: #eee; } .item.checked { background: url('../images/check.png') 0 8px no-repeat; color: #41ad49; cursor: default; } .desc { color: #aaa; line-height: 1.625em; margin: 0; padding-bottom: .3rem; } .desc b { color: #777; } .about{ color: #aaa; } .donate-icon{ display: none; width: 100%; } [lang^="zh"] .zfb{ display: block; } [lang^="en"] .paypal{ display: block; } #color-preview{ border-radius: 3px; display: inline-block; width: 20px; height: 1em; padding: 3px; vertical-align: middle; } #color{ outline: 0; width: 6em; } #color-save-success{ display: none; margin: 5px 0 0; color: #aaa; font-size: 12px; } ================================================ FILE: js/main.js ================================================ var CACHED_STYLES = new Map(); /** * 获取元素样式 * */ const getStyle = (node, key) => { return window.getComputedStyle(node)[key]; }; // 设置元素样式,同时缓存它原有的样式 const setStyle = (node, key, val) => { // 获取原始样式 const originStyle = getStyle(node, key), styleCache = CACHED_STYLES.get(node) || {}; // 把原始样式保存下来 if (!styleCache[key]) { CACHED_STYLES.set(node, { ...styleCache, [key]: originStyle, }); } // 修改为新样式,以前这里可以直接同步改,但react流行之后似乎有些使用react的页面我们直接修改会造成react报错,例如知乎 // 试了一下解决方式就是我们把样式修改改为异步进行,让react先完成她的执行周期就不会有冲突了 setTimeout(() => { node.style[key] = val; }, 0); }; /** * 获取rgba数组 * */ const parseRGBA = (str) => { const rgba = str.match(/[\d\.]+/g); return [Number(rgba[0]), Number(rgba[1]), Number(rgba[2]), rgba.length == 4 ? Number(rgba[3]) : 1]; }; /** * 计算亮度 * */ const calcBrightness = (colorString) => { const rgba = parseRGBA(colorString); // alpha通道为0是transparent if (!rgba[3]) return false; // 把RGB转换为亮度 return (0.2126 * rgba[0]) / 255 + (0.7152 * rgba[1]) / 255 + (0.072 * rgba[2]) / 255; }; const getNodeStyleBrightness = (node, key) => { // 读取颜色数据 const colorString = getStyle(node, key); return colorString ? calcBrightness(colorString) : false; }; /** * 根据预设的class列表跳过特定div,直接用IndexOf是因为这样highlight/highlight/highlighter之类的不用重复了 * @return true dom中包含需要跳过的class * @return false 不包含 * */ const shouldBeIgnored = (node) => { if (OPTIONS.skipNodeTypes.includes(node.nodeName)) { return true; } const classnames = node.getAttribute("class")?.toLowerCase() || ""; const _id = node.id?.toLowerCase() || ""; if (!classnames && !_id) { return false; } for (const ic of OPTIONS.ignoreClass) { if (classnames.includes(ic) || _id.includes(ic)) { return true; } } return false; }; /* * 替换颜色 * @return 3 设置了背景色,且背景色亮度超过阈值,替换了背景色 * @return 2 设置了背景色,但背景色亮度没有超过阈值,没有替换背景色 * @return 1 元素没有设置背景色 * @return 0 此元素依照config中的设置跳过不处理 */ const replaceBackgroundColor = (node) => { // input[type=text],用户选择「不替换输入框颜色」 if (node.nodeName == "INPUT" && !OPTIONS.basic.replaceTextInput) { return 0; } // 根据亮度判断是否需要替换 const brightness = getNodeStyleBrightness(node, "background-color"); if (!brightness) return 1; if (brightness > OPTIONS.basic.bgColorBrightnessThreshold) { setStyle(node, "background-color", OPTIONS.basic.replaceBgWithColor); return 3; } else { return 2; } }; /** * 修改Border颜色 * */ const replaceBorderColor = (node) => { // 四边各自计算 const sides = ["top", "bottom", "left", "right"]; const borderWidthAttrString = "border-%s-width", borderColorAttrString = "border-%s-color"; let borderColorAttr, borderWidth, borderBrightness; for (const side of sides) { // 先判断下是否有边框 borderWidth = getStyle(node, borderWidthAttrString.replace("%s", side)); if (!borderWidth) continue; // 然后判断是否需要替换颜色 borderColorAttr = borderColorAttrString.replace("%s", side); borderBrightness = getNodeStyleBrightness(node, borderColorAttr); if (!borderBrightness) continue; if (borderBrightness > OPTIONS.basic.borderColorBrightnessThreshold) { setStyle(node, borderColorAttr, OPTIONS.basic.replaceBorderWithColor); } } // box-shadow,如果有位移和扩散都为0的box-shadow,我们就认为这个box-shadow是border const shadow = getStyle(node, "box-shadow"); const m = /(rgb\(\d+, \d+, \d+\)) 0px 0px 0px (\d+)px/.exec(shadow); if (m && calcBrightness(m[1])) { setStyle(node, "box-shadow", `${OPTIONS.basic.replaceBorderWithColor} 0px 0px 0px ${m[2]}px`); } }; /** * 修改文字颜色 * */ const replaceTextColor = (node) => { if (!OPTIONS.basic.replaceTextColor) return false; // 文字亮度 const brightness = getNodeStyleBrightness(node, "color"); if (!brightness) return false; // 确认此元素亮度过高且没有背景图片 const bgImage = getStyle(node, "background-image"); if (brightness > OPTIONS.basic.borderColorBrightnessThreshold && (!bgImage || bgImage == "none")) { // 替换文字颜色 setStyle(node, "color", "#000"); } // TODO: 有时候虽然当前元素没有背景图片,但其实文字浮动在父元素或其他元素的背景图片上,造成文字看不清 }; /** * 替换颜色啦啦啦 * @param bool processOther 是否处理边框、文字等其他颜色,此参数继承 * */ const replaceColor = (node, processOther = false) => { // nodeType != 1 表示这不是一个正常的element: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType // 包含highlight/player等特征的节点应当直接跳过,其子节点也不必再遍历 if (node.nodeType !== 1 || shouldBeIgnored(node)) { return; } // 替换背景色 const bgColorReplacReturn = replaceBackgroundColor(node); // 根据是否替换了背景色决定是否要处理边框、文字颜色等 // 当返回值为2、3时说明当前节点是有背景色的,应当根据当前节点的情况修改processOther // 其他情况继续沿用父节点传下来的值 if (bgColorReplacReturn == 3) { processOther = true; } else if (bgColorReplacReturn == 2) { processOther = false; } if (processOther) { // 替换边框色 replaceBorderColor(node); // 替换文本颜色 replaceTextColor(node); } // 递归 node.childNodes.forEach((child) => replaceColor(child, processOther)); benchmark.tick(); }; /** * 回复页面原始样式 * */ const restoreColor = () => { const modifiedNodes = CACHED_STYLES.keys(); for (const node of modifiedNodes) { // 感觉这里会存在内存泄漏,一个dom如果已经被页面逻辑移除了,但是却被eye-protector缓存下来了那是不是就永远不会被释放了? const cahcedStyles = CACHED_STYLES.get(node); for (const key in cahcedStyles) { setStyle(node, key, cahcedStyles[key]); } } }; // 用mutationObserver代替监听DOMSubtreeModified事件,后者有性能缺陷: // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events var observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { var len = mutation.addedNodes.length; for (var i = 0; i < len; i++) { var node = mutation.addedNodes[i]; // 先向上遍历一遍祖先,确认是否需要处理当前节点 var ancestor = node, shouldIgnore = false; while ((ancestor = ancestor.parentNode) && ancestor.nodeName != "BODY") { if (shouldBeIgnored(ancestor)) { shouldIgnore = true; break; } } // 文本节点内容改变也会触发mutation,而text并不是正经的node if (!shouldIgnore && node.nodeType == 1) { replaceColor(node); } } }); }); var observerConfig = { childList: true, subtree: true, }; function wait(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } async function init() { await readOption(); if ((OPTIONS.basic.mode == "positive" && OPTIONS.positiveList.includes(host)) || (OPTIONS.basic.mode == "passive" && !OPTIONS.passiveList.includes(host))) { restoreColor(); observer.disconnect(); return false; } // 1. 当系统不处在深色模式时,一切逻辑正常执行 if (!isNightMode) { // always set background to element setStyle(document.documentElement, "backgroundColor", OPTIONS.basic.replaceBgWithColor); // body需要特殊处理,当body的background-color为transparent时实际上页面是白色 // 此时也需要给body设置背景色 const body = document.body, brightness = getNodeStyleBrightness(body, "background-color"); // 全黑时brightness = 0,一定要排除出去 if ((!brightness && brightness !== 0) || brightness > OPTIONS.basic.bgColorBrightnessThreshold) { setStyle(body, "background-color", OPTIONS.basic.replaceBgWithColor); setStyle(body, "transition", "background-color .3s ease"); } // 遍历DOM Array.from(body.children).forEach(replaceColor); // watch dom changes observer.observe(body, observerConfig); } else { // 2. 当系统处于深色模式,且html or body的背景色足够深时,我就简单粗暴的认为这个网站是有深色样式的,跳过一切逻辑让网页原样显示 // 有的网站会把深色样式加在html上有的会在body上,也可能会加在别的地方吧,但我们就只处理两种最常见的情况,其他的就随缘吧 await wait(10); // 网页切换深色模式有时会有一定延迟,我们也稍微等一下 const html = document.querySelector("html"), htmlBrightness = getNodeStyleBrightness(html, "background-color"); const body = document.body, bodyBrightness = getNodeStyleBrightness(body, "background-color"); // 亮度 = 0时是纯黑色,不需要改色 // 亮度为true且小于阈值时不需要改色 if (htmlBrightness === 0 || (htmlBrightness && htmlBrightness < OPTIONS.basic.bgColorBrightnessThreshold) || bodyBrightness === 0 || (bodyBrightness && bodyBrightness < OPTIONS.basic.bgColorBrightnessThreshold)) { return false; } setStyle(body, "background-color", OPTIONS.basic.replaceBgWithColor); setStyle(body, "transition", "background-color .3s ease"); // 这边稍微有点区别,非深色模式时无论body的背景色深浅代码都会遍历DOM树来检查是否有需要变色的元素 // 但是在深色模式下仅Google就会存在误判的情况,所以想了想还是直接全部跳过了,这样可能会有漏网之鱼但是逻辑最简单 Array.from(body.children).forEach(replaceColor); observer.observe(body, observerConfig); } } // benchmark const benchmark = new Benchmark(); // 保存域名 const host = getHost(document.location.href); // 设置改变时重新读取设置 chrome.storage.onChanged.addListener(init); // 检查浏览器是否开启了Night Mode const { matches: isNightMode } = window.matchMedia("(prefers-color-scheme: dark)"); // GO init(); ================================================ FILE: js/option.options.js ================================================ var CLICKLISTENERS = { check: function () { var key = this.id; // 根据选项类型反转选项 var option = OPTIONS.basic[key]; if (typeof option == "boolean") { OPTIONS.basic[key] = !option; } saveOption(); // toggle class this.classList.toggle("checked"); }, radio: function () { var key = this.getAttribute("name"), val = this.id, nodes = $$(".item[name=" + key + "]"); nodes.forEach(function (node) { node.classList.remove("checked"); }); this.classList.add("checked"); OPTIONS.basic[key] = this.id; saveOption(); }, pickColor: function () { var color = this.value.trim().replace(/[^0-9a-fA-F]/g, ""), preview = $("color-preview"), btn = $("color-save"); // 确保color中不包含非0-9a-fA-F的字符,且长度为3或6 if (color == this.value && (color.length == 3 || color.length == 6)) { preview.style.backgroundColor = "#" + color; btn.disabled = false; } else { preview.style.backgroundColor = "#fff"; btn.disabled = true; } }, saveColor: function () { var color = $("color").value.trim().toUpperCase(); if (color.length == 3) { color = [color[0], color[0], color[1], color[1], color[2], color[2]].join(""); } OPTIONS.basic.replaceBgWithColor = "#" + color; saveOption(function () { $("color-save-success").style.display = "block"; }); }, restoreColor: function () { var color = OPTIONS.basic.defaultBgColor; OPTIONS.basic.replaceBgWithColor = color; saveOption(function () { $("color-preview").style.backgroundColor = color; $("color").value = color.slice(1); }); }, }; function onRangeInput(e) { $("current-brightness").textContent = this.value; } function onRangeChange(e) { OPTIONS.basic.bgColorBrightnessThreshold = this.value; saveOption(); } async function init() { // i18n i18n(); // 读取设置 await readOption(); var options = OPTIONS.basic, key, node; for (key in options) { if (options[key] === true) { node = $(key); if (node) { node.classList.add("checked"); } } } // 工作模式暂时没想到怎么做通用,就做个特例吧 $(OPTIONS.basic.mode).classList.add("checked"); // 背景色 $("color-preview").style.backgroundColor = OPTIONS.basic.replaceBgWithColor; $("color").value = OPTIONS.basic.replaceBgWithColor.slice(1); // 亮度阈值 $("brightness").value = OPTIONS.basic.bgColorBrightnessThreshold; // 修改设置 var nodes = $$(".item"); nodes.forEach(function (node) { node.addEventListener("click", CLICKLISTENERS[node.getAttribute("type")]); }); // 修改背景色 $("color").addEventListener("input", CLICKLISTENERS.pickColor); $("color-save").addEventListener("click", CLICKLISTENERS.saveColor); $("color-restore").addEventListener("click", CLICKLISTENERS.restoreColor); // 亮度阈值 $("brightness").addEventListener("input", onRangeInput); $("brightness").addEventListener("change", onRangeChange); // options页面在打开的情况下,如果用户切到其他tabs修改了OPTIONS,这边需要及时更新 // 不然可能出现options页面打开太久,OPTIONS中的list已经不是最新的,然后options触发了一次saveOptions造成 // options页面打开之后的修改丢失的情况 chrome.storage.onChanged.addListener(readOption); } init(); ================================================ FILE: js/option.popup.js ================================================ var url, host; function onClick() { var list = OPTIONS[this.id + "List"]; if (list.indexOf(host) > -1) { list.remove(host); } else { list.push(host); } saveOption(); // toggle class this.classList.toggle("checked"); } function init() { i18n(); // 获取当前激活tab的域名 chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) { (url = tabs[0].url), (host = getHost(url)); if (!host) { $("normal").style.display = "none"; $("error").textContent = _("strProtocolMsg"); return; } // 读取当前域名的配置 await readOption(); var mode = OPTIONS.basic.mode, list = OPTIONS[mode + "List"], btn = $(mode); // 显示当前模式 $("mode").textContent = mode == "positive" ? _("strPositive") : _("strPassive"); $("mode").href = chrome.runtime.getURL("/options.html"); // 根据运行模式产生按钮状态 btn.style.display = "block"; if (list.indexOf(host) > -1) { btn.classList.add("checked"); } }); // 修改设置 $$(".item").forEach(function (node) { node.addEventListener("click", onClick); }); } init(); ================================================ FILE: js/utility.js ================================================ var OPTIONS = { basic: { // 工作模式 mode: "positive", // 背景色 replaceBgWithColor: "#C1E6C6", // 豆沙绿 defaultBgColor: "#C1E6C6", // 替换背景色的亮度阈值 bgColorBrightnessThreshold: 0.9, // 边框色 replaceBorderWithColor: "rgba(0, 0, 0, .35)", // 替换边框色的亮度阈值 borderColorBrightnessThreshold: 0.5, // 是否替换文字颜色 replaceTextColor: true, // 是否替换文本输入框背景色 replaceTextInput: false, }, // 忽略的特殊class ignoreClass: ["highlight", "syntax", "code", "player"], // 忽略的特殊nodeType skipNodeTypes: ["SCRIPT", "BR", "CANVAS", "IMG", "svg", "CODE"], // 主动模式 - 忽略的网站列表 positiveList: [], // 被动模式 - 要替换的域名列表 passiveList: [], }; // 检查对象是否为空 function is_object_empty(obj) { for (var key in obj) { return false; } return true; } // array.remove Array.prototype.remove = function (key) { var index = this.indexOf(key); if (index > -1) this.splice(index, 1); return this; }; // shortcut function $(id) { return document.getElementById(id); } function $$(selector) { return Array.from(document.querySelectorAll(selector)); } var storage = chrome.storage.sync; function readOption() { return new Promise((resolve) => { storage.get("option", function (obj) { if (obj.option && obj.option.basic) { OPTIONS = obj["option"]; } // add ignoreNodeTypes to defaultOption if (!OPTIONS.skipNodeTypes) { OPTIONS.skipNodeTypes = ["SCRIPT", "BR", "CANVAS", "IMG", "svg", "CODE"]; saveOption(); } // resolve(); }); }); } function saveOption(callback) { storage.set({ option: OPTIONS }, callback); } // 获取当前激活标签页域名 function getHost(url) { var host = /https?:\/\/([^/]+)\//.exec(url); if (host && host.length > 1) { host = host[1]; if (host.startsWith("www.")) { return host.slice(4); } else { return host; } } else { return false; } } // i18n function _(msg) { return chrome.i18n.getMessage(msg); } function i18n() { // render texts var nodes = $$("[data-text]"); nodes.forEach(function (node) { node.innerHTML = _(node.dataset.text); }); // add language to body document.body.setAttribute("lang", chrome.i18n.getUILanguage()); } // benchmark class Benchmark { constructor() { this.start = new Date(); this.end = null; this.ticker = null; } tick() { this.end = new Date(); clearTimeout(this.ticker); this.ticker = setTimeout(() => { const elapsed = (this.end - this.start) / 1000; console.log(`[Eyeprotector] Runs for ${elapsed.toFixed(2)}s`); // 只记录初始化那波就够了 this.tick = () => {}; }, 999); } } ================================================ FILE: manifest.json ================================================ { "manifest_version": 3, "name": "__MSG_extName__", "description": "__MSG_extDescription__", "version": "2.4", "permissions": [ "storage", "activeTab" ], "icons":{ "16": "images/icon.png", "48":"images/icon.png", "128":"images/icon.png" }, "action": { "default_icon": { "19": "images/icon.png", "38": "images/icon.png" }, "default_title": "设置设置", "default_popup": "popup.html" }, "browser_specific_settings": { "gecko": { "id": "eye_protector", "strict_min_version": "68.0" } }, "options_ui": { "page": "options.html" }, "content_scripts":[{ "matches":[ "http://*/*", "https://*/*" ], "js":[ "js/utility.js", "js/main.js" ], "run_at": "document_idle" }], "default_locale": "en" } ================================================ FILE: options.html ================================================
关于
· Made by @千歳TheC
· 代码托管于Github,如果喜欢这个扩展请给我一个Star,谢谢。
如果你愿意赞助我一杯可乐就太感谢了:)
工作模式
主动模式

默认替换所有网页的颜色,您可以在不需要替换颜色的域名下关闭护眼功能。

被动模式

默认不替换任何网页的颜色,您需要自行设置哪些域名需要开启护眼功能。

全局设置
文字替换为黑色
替换输入框颜色
自定义背景色
#
亮度阈值

设置一个适合你的亮度阈值

(一般来说直接用默认值就可以了)

保存成功

================================================ FILE: popup.html ================================================
此域名启用护眼模式
此域名关闭护眼模式

正在下运行

================================================ FILE: readme.md ================================================ Eye Protector --- This extension tries its best to keep your page clean and tidy while removing colors which are too bright. You can find details in [Chrome Store](https://chrome.google.com/webstore/detail/%E4%BF%9D%E6%8A%A4%E7%9C%BC%E7%9D%9B/fgadnbmmolnmbkbklpaojbogcopipopl).