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 <a href='' target='_blank' id='mode'></a>"
},
"strAbout": {
"message": "About"
},
"strRepository": {
"message": "· Source code on <a href='https://github.com/chitosai/eye_protector' target='_blank'>Github</a>. If you like the extension, please give me a star, thanks :)"
},
"strMode": {
"message": "Mode"
},
"strPositiveModeDesc": {
"message": "Positive<p class='desc'><b>Replace</b> background color for all domains, you can disable eye-protect for particular domains.</p>"
},
"strPassiveModeDesc": {
"message": "Passive<p class='desc'><b>Not replace</b> background color by default, you can enable eye-protect for particular domains mamually.</p>"
},
"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": "正在<a href='' target='_blank' id='mode'></a>下运行"
},
"strAbout": {
"message": "关于"
},
"strRepository": {
"message": "· 代码托管于<a href='https://github.com/chitosai/eye_protector' target='_blank'>Github</a>,如果喜欢这个扩展请给我一个Star,谢谢:)"
},
"strMode": {
"message": "工作模式"
},
"strPositiveModeDesc": {
"message": "主动模式<p class='desc'>默认<b>替换</b>所有网页的颜色,您可以在不需要替换颜色的域名下关闭护眼功能。</p>"
},
"strPassiveModeDesc": {
"message": "被动模式<p class='desc'>默认<b>不替换</b>任何网页的颜色,您需要自行设置哪些域名需要开启护眼功能。</p>"
},
"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 <html> 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
================================================
<!doctype html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/options.css">
</head>
<body id="option-page">
<div id="col-2">
<div class="title" data-text="strAbout">关于</div>
<div class="about">· Made by <a href="http://weibo.com/chitosai" target="_blank">@千歳TheC</a></div>
<div class="about" data-text="strRepository">· 代码托管于<a href='https://github.com/chitosai/eye_protector' target='_blank'>Github</a>,如果喜欢这个扩展请给我一个Star,谢谢。</div>
<div class="about" style="margin: 15px 0;" data-text="strDonate">如果你愿意赞助我一杯可乐就太感谢了:)</div>
<img class="donate-icon zfb" src="images/zfb.png" alt="支付宝">
<img class="donate-icon paypal" src="images/paypal.jpg" alt="Paypal">
</div>
<div id="col-1">
<div class="title" data-text="strMode">工作模式</div>
<div class="list">
<div class="item" type="radio" id="positive" name="mode" data-text="strPositiveModeDesc">
主动模式
<p class="desc">默认<b>替换</b>所有网页的颜色,您可以在不需要替换颜色的域名下关闭护眼功能。</p>
</div>
<div class="item" type="radio" id="passive" name="mode" data-text="strPassiveModeDesc">
被动模式
<p class="desc">默认<b>不替换</b>任何网页的颜色,您需要自行设置哪些域名需要开启护眼功能。</p>
</div>
</div>
<div class="title" data-text="strGlobalSetting">全局设置</div>
<div class="list">
<div class="item" type="check" id="replaceTextColor" data-text="strReplaceTextColor">文字替换为黑色</div>
<div class="item" type="check" id="replaceTextInput" data-text="strReplaceInputColor">替换输入框颜色</div>
</div>
<div class="title" data-text="strCustomBgColor">自定义背景色</div>
<div class="color">
<span id="color-preview"></span>
#<input type="text" id="color">
<button id="color-save" data-text="strSave">保存</button>
<button id="color-restore" data-text="strRestore">还原为豆沙绿</button>
</div>
<div class="title" data-text="strBrightnessThreshold">亮度阈值</div>
<div class="brightness">
<p data-text="strBrightnessThresholdTip1">设置一个适合你的亮度阈值</p>
<p data-text="strBrightnessThresholdTip2" style="color: #aaa; margin: -15px 0 15px;">(一般来说直接用默认值就可以了)</p>
<input id="brightness" type="range" min="0" max="0.99" step="0.01">
<span id="current-brightness"></span>
</div>
<p id="color-save-success" data-text="strSaveSuccess">保存成功</p>
</div>
<!-- end -->
<script src="js/utility.js"></script>
<script src="js/option.options.js"></script>
</body>
</html>
================================================
FILE: popup.html
================================================
<!doctype html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<style>
body{
font: 14px "PingFangSC-Light", "Segoe UI", "Microsoft YaHei";
width: 160px;
margin: 0;
}
body[lang=en] .item,
body[lang=en-US] .item,
body[lang=en-GB] .item{
font-size: 12px;
}
#error{
color: #aaa;
padding: .5rem;
}
#error:empty{
display: none;
}
.list {
padding: .5rem;
}
.item {
border-radius: 3px;
cursor: pointer;
display: none;
line-height: 2em;
text-align: center;
margin-bottom: 1px
}
.item:hover {
background: #eee;
}
.item.checked {
background: #41ad49;
color: #fff;
cursor: default;
}
#current{
background: #f7f7f7;
border-top: 1px solid #e3e3e3;
color: #888;
font-size: 10px;
line-height: 3em;
margin: 0;
text-align: center;
}
#mode{
color: #000;
outline: 0;
}
</style>
</head>
<body>
<div id="normal">
<div class="list">
<div class="item" id="passive" data-text="strEnableForCurrentDomain">此域名启用护眼模式</div>
<div class="item" id="positive" data-text="strDisableForCurrentDomain">此域名关闭护眼模式</div>
</div>
<p id="current" data-text="strCurrentMode">正在<a href="" target="_blank" id="mode"></a>下运行</p>
</div>
<div id="error"></div>
<!-- end -->
<script src="js/utility.js"></script>
<script src="js/option.popup.js"></script>
</body>
</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).
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
SYMBOL INDEX (18 symbols across 4 files)
FILE: js/main.js
function wait (line 241) | function wait(ms) {
function init (line 247) | async function init() {
FILE: js/option.options.js
function onRangeInput (line 61) | function onRangeInput(e) {
function onRangeChange (line 65) | function onRangeChange(e) {
function init (line 70) | async function init() {
FILE: js/option.popup.js
function onClick (line 3) | function onClick() {
function init (line 17) | function init() {
FILE: js/utility.js
function is_object_empty (line 31) | function is_object_empty(obj) {
function $ (line 46) | function $(id) {
function $$ (line 49) | function $$(selector) {
function readOption (line 55) | function readOption() {
function saveOption (line 72) | function saveOption(callback) {
function getHost (line 77) | function getHost(url) {
function _ (line 92) | function _(msg) {
function i18n (line 95) | function i18n() {
class Benchmark (line 107) | class Benchmark {
method constructor (line 108) | constructor() {
method tick (line 113) | tick() {
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (31K chars).
[
{
"path": "_locales/en/messages.json",
"chars": 1978,
"preview": "{\n\t\"extName\": {\n\t\t\"message\": \"Eye Protector\"\n\t},\n\t\"extDescription\": {\n\t\t\"message\": \"May it be the best eye-protect exten"
},
{
"path": "_locales/zh_CN/messages.json",
"chars": 1470,
"preview": "{\n\t\"extName\": {\n\t\t\"message\": \"保护眼睛\"\n\t},\n\t\"extDescription\": {\n\t\t\"message\": \"阿姆斯特朗回旋喷气式阿姆斯特朗墨镜\"\n\t},\n\t\"strProtocolMsg\": {\n\t"
},
{
"path": "css/options.css",
"chars": 1189,
"preview": "body {\n font: 14px \"PingFangSC-Light\", \"Segoe UI\", \"Microsoft YaHei\";\n margin: .6rem;\n width: 150px;\n}\n#option-page {"
},
{
"path": "js/main.js",
"chars": 8663,
"preview": "var CACHED_STYLES = new Map();\n\n/**\n * 获取元素样式\n *\n */\nconst getStyle = (node, key) => {\n return window.getComputedStyle("
},
{
"path": "js/option.options.js",
"chars": 3168,
"preview": "var CLICKLISTENERS = {\n check: function () {\n var key = this.id;\n\n // 根据选项类型反转选项\n var option = OPTIONS.basic[k"
},
{
"path": "js/option.popup.js",
"chars": 1099,
"preview": "var url, host;\n\nfunction onClick() {\n var list = OPTIONS[this.id + \"List\"];\n\n if (list.indexOf(host) > -1) {\n list."
},
{
"path": "js/utility.js",
"chars": 2637,
"preview": "var OPTIONS = {\n basic: {\n // 工作模式\n mode: \"positive\",\n // 背景色\n replaceBgWithColor: \"#C1E6C6\",\n // 豆沙绿\n "
},
{
"path": "manifest.json",
"chars": 819,
"preview": "{\n \"manifest_version\": 3,\n\n \"name\": \"__MSG_extName__\",\n \"description\": \"__MSG_extDescription__\",\n \"version\": \"2.4\",\n"
},
{
"path": "options.html",
"chars": 2327,
"preview": "<!doctype html>\n<html lang=\"cn\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<link rel=\"stylesheet\" href=\"css/options.css\">\n</head>\n"
},
{
"path": "popup.html",
"chars": 1395,
"preview": "<!doctype html>\n<html lang=\"cn\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<style>\n\t\tbody{\n\t\t\tfont: 14px \"PingFangSC-Light\", \"Sego"
},
{
"path": "readme.md",
"chars": 279,
"preview": "Eye Protector\n---\n\nThis extension tries its best to keep your page clean and tidy while removing colors which are too br"
}
]
About this extraction
This page contains the full source code of the chitosai/eye_protector GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (24.4 KB), approximately 8.4k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.