5163 lines
189 KiB
JavaScript
5163 lines
189 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const userAgent =
|
|
typeof navigator === "undefined"
|
|
? "some useragent"
|
|
: navigator.userAgent.toLowerCase();
|
|
const platform =
|
|
typeof navigator === "undefined"
|
|
? "some platform"
|
|
: navigator.platform.toLowerCase();
|
|
const isChromium =
|
|
userAgent.includes("chrome") || userAgent.includes("chromium");
|
|
const isThunderbird = userAgent.includes("thunderbird");
|
|
const isFirefox = userAgent.includes("firefox") || isThunderbird;
|
|
userAgent.includes("vivaldi");
|
|
userAgent.includes("yabrowser");
|
|
userAgent.includes("opr") || userAgent.includes("opera");
|
|
const isEdge = userAgent.includes("edg");
|
|
userAgent.includes("safari") && !isChromium;
|
|
const isWindows = platform.startsWith("win");
|
|
const isMacOS = platform.startsWith("mac");
|
|
userAgent.includes("mobile");
|
|
const chromiumVersion = (() => {
|
|
const m = userAgent.match(/chrom[e|ium]\/([^ ]+)/);
|
|
if (m && m[1]) {
|
|
return m[1];
|
|
}
|
|
return "";
|
|
})();
|
|
(() => {
|
|
try {
|
|
document.querySelector(":defined");
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
})();
|
|
function compareChromeVersions($a, $b) {
|
|
const a = $a.split(".").map((x) => parseInt(x));
|
|
const b = $b.split(".").map((x) => parseInt(x));
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i]) {
|
|
return a[i] < b[i] ? -1 : 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
const isXMLHttpRequestSupported = typeof XMLHttpRequest === "function";
|
|
const isFetchSupported = typeof fetch === "function";
|
|
const isMV3 =
|
|
globalThis.chrome &&
|
|
globalThis.chrome.runtime &&
|
|
globalThis.chrome.runtime.getManifest &&
|
|
globalThis.chrome.runtime.getManifest().manifest_version === 3;
|
|
|
|
async function getOKResponse(url, mimeType, origin) {
|
|
const response = await fetch(url, {
|
|
cache: "force-cache",
|
|
credentials: "omit",
|
|
referrer: origin
|
|
});
|
|
if (
|
|
isFirefox &&
|
|
mimeType === "text/css" &&
|
|
url.startsWith("moz-extension://") &&
|
|
url.endsWith(".css")
|
|
) {
|
|
return response;
|
|
}
|
|
if (
|
|
mimeType &&
|
|
!response.headers.get("Content-Type").startsWith(mimeType)
|
|
) {
|
|
throw new Error(`Mime type mismatch when loading ${url}`);
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Unable to load ${url} ${response.status} ${response.statusText}`
|
|
);
|
|
}
|
|
return response;
|
|
}
|
|
async function loadAsDataURL(url, mimeType) {
|
|
const response = await getOKResponse(url, mimeType);
|
|
return await readResponseAsDataURL(response);
|
|
}
|
|
async function readResponseAsDataURL(response) {
|
|
const blob = await response.blob();
|
|
const dataURL = await new Promise((resolve) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => resolve(reader.result);
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
return dataURL;
|
|
}
|
|
async function loadAsText(url, mimeType, origin) {
|
|
const response = await getOKResponse(url, mimeType, origin);
|
|
return await response.text();
|
|
}
|
|
|
|
function parseArray(text) {
|
|
return text
|
|
.replace(/\r/g, "")
|
|
.split("\n")
|
|
.map((s) => s.trim())
|
|
.filter((s) => s);
|
|
}
|
|
function formatArray(arr) {
|
|
return arr.concat("").join("\n");
|
|
}
|
|
function getStringSize(value) {
|
|
return value.length * 2;
|
|
}
|
|
|
|
function parse24HTime(time) {
|
|
return time.split(":").map((x) => parseInt(x));
|
|
}
|
|
function compareTime(time1, time2) {
|
|
if (time1[0] === time2[0] && time1[1] === time2[1]) {
|
|
return 0;
|
|
}
|
|
if (
|
|
time1[0] < time2[0] ||
|
|
(time1[0] === time2[0] && time1[1] < time2[1])
|
|
) {
|
|
return -1;
|
|
}
|
|
return 1;
|
|
}
|
|
function nextTimeInterval(time0, time1, date = new Date()) {
|
|
const a = parse24HTime(time0);
|
|
const b = parse24HTime(time1);
|
|
const t = [date.getHours(), date.getMinutes()];
|
|
if (compareTime(a, b) > 0) {
|
|
return nextTimeInterval(time1, time0, date);
|
|
}
|
|
if (compareTime(a, b) === 0) {
|
|
return null;
|
|
}
|
|
if (compareTime(t, a) < 0) {
|
|
date.setHours(a[0]);
|
|
date.setMinutes(a[1]);
|
|
return date.getTime();
|
|
}
|
|
if (compareTime(t, b) < 0) {
|
|
date.setHours(b[0]);
|
|
date.setMinutes(b[1]);
|
|
return date.getTime();
|
|
}
|
|
return new Date(
|
|
date.getFullYear(),
|
|
date.getMonth(),
|
|
date.getDate() + 1,
|
|
a[0],
|
|
a[1]
|
|
).getTime();
|
|
}
|
|
function isInTimeIntervalLocal(time0, time1, date = new Date()) {
|
|
const a = parse24HTime(time0);
|
|
const b = parse24HTime(time1);
|
|
const t = [date.getHours(), date.getMinutes()];
|
|
if (compareTime(a, b) > 0) {
|
|
return compareTime(a, t) <= 0 || compareTime(t, b) < 0;
|
|
}
|
|
return compareTime(a, t) <= 0 && compareTime(t, b) < 0;
|
|
}
|
|
function isInTimeIntervalUTC(time0, time1, timestamp) {
|
|
if (time1 < time0) {
|
|
return timestamp <= time1 || time0 <= timestamp;
|
|
}
|
|
return time0 < timestamp && timestamp < time1;
|
|
}
|
|
function getDuration(time) {
|
|
let duration = 0;
|
|
if (time.seconds) {
|
|
duration += time.seconds * 1000;
|
|
}
|
|
if (time.minutes) {
|
|
duration += time.minutes * 60 * 1000;
|
|
}
|
|
if (time.hours) {
|
|
duration += time.hours * 60 * 60 * 1000;
|
|
}
|
|
if (time.days) {
|
|
duration += time.days * 24 * 60 * 60 * 1000;
|
|
}
|
|
return duration;
|
|
}
|
|
function getDurationInMinutes(time) {
|
|
return getDuration(time) / 1000 / 60;
|
|
}
|
|
function getSunsetSunriseUTCTime(latitude, longitude, date) {
|
|
const dec31 = Date.UTC(date.getUTCFullYear(), 0, 0, 0, 0, 0, 0);
|
|
const oneDay = getDuration({days: 1});
|
|
const dayOfYear = Math.floor((date.getTime() - dec31) / oneDay);
|
|
const zenith = 90.83333333333333;
|
|
const D2R = Math.PI / 180;
|
|
const R2D = 180 / Math.PI;
|
|
const lnHour = longitude / 15;
|
|
function getTime(isSunrise) {
|
|
const t = dayOfYear + ((isSunrise ? 6 : 18) - lnHour) / 24;
|
|
const M = 0.9856 * t - 3.289;
|
|
let L =
|
|
M +
|
|
1.916 * Math.sin(M * D2R) +
|
|
0.02 * Math.sin(2 * M * D2R) +
|
|
282.634;
|
|
if (L > 360) {
|
|
L -= 360;
|
|
} else if (L < 0) {
|
|
L += 360;
|
|
}
|
|
let RA = R2D * Math.atan(0.91764 * Math.tan(L * D2R));
|
|
if (RA > 360) {
|
|
RA -= 360;
|
|
} else if (RA < 0) {
|
|
RA += 360;
|
|
}
|
|
const Lquadrant = Math.floor(L / 90) * 90;
|
|
const RAquadrant = Math.floor(RA / 90) * 90;
|
|
RA += Lquadrant - RAquadrant;
|
|
RA /= 15;
|
|
const sinDec = 0.39782 * Math.sin(L * D2R);
|
|
const cosDec = Math.cos(Math.asin(sinDec));
|
|
const cosH =
|
|
(Math.cos(zenith * D2R) - sinDec * Math.sin(latitude * D2R)) /
|
|
(cosDec * Math.cos(latitude * D2R));
|
|
if (cosH > 1) {
|
|
return {
|
|
alwaysDay: false,
|
|
alwaysNight: true,
|
|
time: 0
|
|
};
|
|
} else if (cosH < -1) {
|
|
return {
|
|
alwaysDay: true,
|
|
alwaysNight: false,
|
|
time: 0
|
|
};
|
|
}
|
|
const H =
|
|
(isSunrise
|
|
? 360 - R2D * Math.acos(cosH)
|
|
: R2D * Math.acos(cosH)) / 15;
|
|
const T = H + RA - 0.06571 * t - 6.622;
|
|
let UT = T - lnHour;
|
|
if (UT > 24) {
|
|
UT -= 24;
|
|
} else if (UT < 0) {
|
|
UT += 24;
|
|
}
|
|
return {
|
|
alwaysDay: false,
|
|
alwaysNight: false,
|
|
time: Math.round(UT * getDuration({hours: 1}))
|
|
};
|
|
}
|
|
const sunriseTime = getTime(true);
|
|
const sunsetTime = getTime(false);
|
|
if (sunriseTime.alwaysDay || sunsetTime.alwaysDay) {
|
|
return {
|
|
alwaysDay: true
|
|
};
|
|
} else if (sunriseTime.alwaysNight || sunsetTime.alwaysNight) {
|
|
return {
|
|
alwaysNight: true
|
|
};
|
|
}
|
|
return {
|
|
sunriseTime: sunriseTime.time,
|
|
sunsetTime: sunsetTime.time
|
|
};
|
|
}
|
|
function isNightAtLocation(latitude, longitude, date = new Date()) {
|
|
const time = getSunsetSunriseUTCTime(latitude, longitude, date);
|
|
if (time.alwaysDay) {
|
|
return false;
|
|
} else if (time.alwaysNight) {
|
|
return true;
|
|
}
|
|
const sunriseTime = time.sunriseTime;
|
|
const sunsetTime = time.sunsetTime;
|
|
const currentTime =
|
|
date.getUTCHours() * getDuration({hours: 1}) +
|
|
date.getUTCMinutes() * getDuration({minutes: 1}) +
|
|
date.getUTCSeconds() * getDuration({seconds: 1}) +
|
|
date.getUTCMilliseconds();
|
|
return isInTimeIntervalUTC(sunsetTime, sunriseTime, currentTime);
|
|
}
|
|
function nextTimeChangeAtLocation(latitude, longitude, date = new Date()) {
|
|
const time = getSunsetSunriseUTCTime(latitude, longitude, date);
|
|
if (time.alwaysDay) {
|
|
return date.getTime() + getDuration({days: 1});
|
|
} else if (time.alwaysNight) {
|
|
return date.getTime() + getDuration({days: 1});
|
|
}
|
|
const [firstTimeOnDay, lastTimeOnDay] =
|
|
time.sunriseTime < time.sunsetTime
|
|
? [time.sunriseTime, time.sunsetTime]
|
|
: [time.sunsetTime, time.sunriseTime];
|
|
const currentTime =
|
|
date.getUTCHours() * getDuration({hours: 1}) +
|
|
date.getUTCMinutes() * getDuration({minutes: 1}) +
|
|
date.getUTCSeconds() * getDuration({seconds: 1}) +
|
|
date.getUTCMilliseconds();
|
|
if (currentTime <= firstTimeOnDay) {
|
|
return Date.UTC(
|
|
date.getUTCFullYear(),
|
|
date.getUTCMonth(),
|
|
date.getUTCDate(),
|
|
0,
|
|
0,
|
|
0,
|
|
firstTimeOnDay
|
|
);
|
|
}
|
|
if (currentTime <= lastTimeOnDay) {
|
|
return Date.UTC(
|
|
date.getUTCFullYear(),
|
|
date.getUTCMonth(),
|
|
date.getUTCDate(),
|
|
0,
|
|
0,
|
|
0,
|
|
lastTimeOnDay
|
|
);
|
|
}
|
|
return Date.UTC(
|
|
date.getUTCFullYear(),
|
|
date.getUTCMonth(),
|
|
date.getUTCDate() + 1,
|
|
0,
|
|
0,
|
|
0,
|
|
firstTimeOnDay
|
|
);
|
|
}
|
|
|
|
async function readText(params) {
|
|
return new Promise((resolve, reject) => {
|
|
if (isXMLHttpRequestSupported) {
|
|
const request = new XMLHttpRequest();
|
|
request.overrideMimeType("text/plain");
|
|
request.open("GET", params.url, true);
|
|
request.onload = () => {
|
|
if (request.status >= 200 && request.status < 300) {
|
|
resolve(request.responseText);
|
|
} else {
|
|
reject(
|
|
new Error(
|
|
`${request.status}: ${request.statusText}`
|
|
)
|
|
);
|
|
}
|
|
};
|
|
request.onerror = () =>
|
|
reject(
|
|
new Error(`${request.status}: ${request.statusText}`)
|
|
);
|
|
if (params.timeout) {
|
|
request.timeout = params.timeout;
|
|
request.ontimeout = () =>
|
|
reject(
|
|
new Error("File loading stopped due to timeout")
|
|
);
|
|
}
|
|
request.send();
|
|
} else if (isFetchSupported) {
|
|
let abortController;
|
|
let signal;
|
|
let timedOut = false;
|
|
if (params.timeout) {
|
|
abortController = new AbortController();
|
|
signal = abortController.signal;
|
|
setTimeout(() => {
|
|
abortController.abort();
|
|
timedOut = true;
|
|
}, params.timeout);
|
|
}
|
|
fetch(params.url, {signal})
|
|
.then((response) => {
|
|
if (response.status >= 200 && response.status < 300) {
|
|
resolve(response.text());
|
|
} else {
|
|
reject(
|
|
new Error(
|
|
`${response.status}: ${response.statusText}`
|
|
)
|
|
);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (timedOut) {
|
|
reject(
|
|
new Error("File loading stopped due to timeout")
|
|
);
|
|
} else {
|
|
reject(error);
|
|
}
|
|
});
|
|
} else {
|
|
reject(
|
|
new Error(
|
|
`Neither XMLHttpRequest nor Fetch API are accessible!`
|
|
)
|
|
);
|
|
}
|
|
});
|
|
}
|
|
class LimitedCacheStorage {
|
|
constructor() {
|
|
this.bytesInUse = 0;
|
|
this.records = new Map();
|
|
this.alarmIsActive = false;
|
|
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
|
if (alarm.name === LimitedCacheStorage.ALARM_NAME) {
|
|
this.alarmIsActive = false;
|
|
this.removeExpiredRecords();
|
|
}
|
|
});
|
|
}
|
|
ensureAlarmIsScheduled() {
|
|
if (!this.alarmIsActive) {
|
|
chrome.alarms.create(LimitedCacheStorage.ALARM_NAME, {
|
|
delayInMinutes: 1
|
|
});
|
|
this.alarmIsActive = true;
|
|
}
|
|
}
|
|
has(url) {
|
|
return this.records.has(url);
|
|
}
|
|
get(url) {
|
|
if (this.records.has(url)) {
|
|
const record = this.records.get(url);
|
|
record.expires = Date.now() + LimitedCacheStorage.TTL;
|
|
this.records.delete(url);
|
|
this.records.set(url, record);
|
|
return record.value;
|
|
}
|
|
return null;
|
|
}
|
|
set(url, value) {
|
|
this.ensureAlarmIsScheduled();
|
|
const size = getStringSize(value);
|
|
if (size > LimitedCacheStorage.QUOTA_BYTES) {
|
|
return;
|
|
}
|
|
for (const [url, record] of this.records) {
|
|
if (this.bytesInUse + size > LimitedCacheStorage.QUOTA_BYTES) {
|
|
this.records.delete(url);
|
|
this.bytesInUse -= record.size;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
const expires = Date.now() + LimitedCacheStorage.TTL;
|
|
this.records.set(url, {url, value, size, expires});
|
|
this.bytesInUse += size;
|
|
}
|
|
removeExpiredRecords() {
|
|
const now = Date.now();
|
|
for (const [url, record] of this.records) {
|
|
if (record.expires < now) {
|
|
this.records.delete(url);
|
|
this.bytesInUse -= record.size;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if (this.records.size !== 0) {
|
|
this.ensureAlarmIsScheduled();
|
|
}
|
|
}
|
|
}
|
|
LimitedCacheStorage.QUOTA_BYTES =
|
|
(navigator.deviceMemory || 4) * 16 * 1024 * 1024;
|
|
LimitedCacheStorage.TTL = getDuration({minutes: 10});
|
|
LimitedCacheStorage.ALARM_NAME = "network";
|
|
function createFileLoader() {
|
|
const caches = {
|
|
"data-url": new LimitedCacheStorage(),
|
|
"text": new LimitedCacheStorage()
|
|
};
|
|
const loaders = {
|
|
"data-url": loadAsDataURL,
|
|
"text": loadAsText
|
|
};
|
|
async function get({url, responseType, mimeType, origin}) {
|
|
const cache = caches[responseType];
|
|
const load = loaders[responseType];
|
|
if (cache.has(url)) {
|
|
return cache.get(url);
|
|
}
|
|
const data = await load(url, mimeType, origin);
|
|
cache.set(url, data);
|
|
return data;
|
|
}
|
|
return {get};
|
|
}
|
|
|
|
function isIPV6(url) {
|
|
const openingBracketIndex = url.indexOf("[");
|
|
if (openingBracketIndex < 0) {
|
|
return false;
|
|
}
|
|
const queryIndex = url.indexOf("?");
|
|
if (queryIndex >= 0 && openingBracketIndex > queryIndex) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
const ipV6HostRegex = /\[.*?\](\:\d+)?/;
|
|
function compareIPV6(firstURL, secondURL) {
|
|
const firstHost = firstURL.match(ipV6HostRegex)[0];
|
|
const secondHost = secondURL.match(ipV6HostRegex)[0];
|
|
return firstHost === secondHost;
|
|
}
|
|
|
|
function getURLHostOrProtocol($url) {
|
|
const url = new URL($url);
|
|
if (url.host) {
|
|
return url.host;
|
|
}
|
|
return url.protocol;
|
|
}
|
|
function compareURLPatterns(a, b) {
|
|
return a.localeCompare(b);
|
|
}
|
|
function isURLInList(url, list) {
|
|
for (let i = 0; i < list.length; i++) {
|
|
if (isURLMatched(url, list[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isURLMatched(url, urlTemplate) {
|
|
const isFirstIPV6 = isIPV6(url);
|
|
const isSecondIPV6 = isIPV6(urlTemplate);
|
|
if (isFirstIPV6 && isSecondIPV6) {
|
|
return compareIPV6(url, urlTemplate);
|
|
} else if (!isFirstIPV6 && !isSecondIPV6) {
|
|
const regex = createUrlRegex(urlTemplate);
|
|
return Boolean(url.match(regex));
|
|
}
|
|
return false;
|
|
}
|
|
function createUrlRegex(urlTemplate) {
|
|
urlTemplate = urlTemplate.trim();
|
|
const exactBeginning = urlTemplate[0] === "^";
|
|
const exactEnding = urlTemplate[urlTemplate.length - 1] === "$";
|
|
urlTemplate = urlTemplate
|
|
.replace(/^\^/, "")
|
|
.replace(/\$$/, "")
|
|
.replace(/^.*?\/{2,3}/, "")
|
|
.replace(/\?.*$/, "")
|
|
.replace(/\/$/, "");
|
|
let slashIndex;
|
|
let beforeSlash;
|
|
let afterSlash;
|
|
if ((slashIndex = urlTemplate.indexOf("/")) >= 0) {
|
|
beforeSlash = urlTemplate.substring(0, slashIndex);
|
|
afterSlash = urlTemplate.replace(/\$/g, "").substring(slashIndex);
|
|
} else {
|
|
beforeSlash = urlTemplate.replace(/\$/g, "");
|
|
}
|
|
let result = exactBeginning
|
|
? "^(.*?\\:\\/{2,3})?"
|
|
: "^(.*?\\:\\/{2,3})?([^/]*?\\.)?";
|
|
const hostParts = beforeSlash.split(".");
|
|
result += "(";
|
|
for (let i = 0; i < hostParts.length; i++) {
|
|
if (hostParts[i] === "*") {
|
|
hostParts[i] = "[^\\.\\/]+?";
|
|
}
|
|
}
|
|
result += hostParts.join("\\.");
|
|
result += ")";
|
|
if (afterSlash) {
|
|
result += "(";
|
|
result += afterSlash.replace("/", "\\/");
|
|
result += ")";
|
|
}
|
|
result += exactEnding ? "(\\/?(\\?[^/]*?)?)$" : "(\\/?.*?)$";
|
|
return new RegExp(result, "i");
|
|
}
|
|
function isPDF(url) {
|
|
if (url.includes(".pdf")) {
|
|
if (url.includes("?")) {
|
|
url = url.substring(0, url.lastIndexOf("?"));
|
|
}
|
|
if (url.includes("#")) {
|
|
url = url.substring(0, url.lastIndexOf("#"));
|
|
}
|
|
if (
|
|
(url.match(/(wikipedia|wikimedia).org/i) &&
|
|
url.match(
|
|
/(wikipedia|wikimedia)\.org\/.*\/[a-z]+\:[^\:\/]+\.pdf/i
|
|
)) ||
|
|
(url.match(/timetravel\.mementoweb\.org\/reconstruct/i) &&
|
|
url.match(/\.pdf$/i))
|
|
) {
|
|
return false;
|
|
}
|
|
if (url.endsWith(".pdf")) {
|
|
for (let i = url.length; i > 0; i--) {
|
|
if (url[i] === "=") {
|
|
return false;
|
|
} else if (url[i] === "/") {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isURLEnabled(url, userSettings, {isProtected, isInDarkList}) {
|
|
if (isProtected && !userSettings.enableForProtectedPages) {
|
|
return false;
|
|
}
|
|
if (isThunderbird) {
|
|
return true;
|
|
}
|
|
if (isPDF(url)) {
|
|
return userSettings.enableForPDF;
|
|
}
|
|
const isURLInUserList = isURLInList(url, userSettings.siteList);
|
|
const isURLInEnabledList = isURLInList(
|
|
url,
|
|
userSettings.siteListEnabled
|
|
);
|
|
if (userSettings.applyToListedOnly && !isURLInEnabledList) {
|
|
return isURLInUserList;
|
|
}
|
|
if (isURLInEnabledList && isInDarkList) {
|
|
return true;
|
|
}
|
|
return !isInDarkList && !isURLInUserList;
|
|
}
|
|
function isFullyQualifiedDomain(candidate) {
|
|
return /^[a-z0-9.-]+$/.test(candidate);
|
|
}
|
|
|
|
function parseSitesFixesConfig(text, options) {
|
|
const sites = [];
|
|
const blocks = text.replace(/\r/g, "").split(/^\s*={2,}\s*$/gm);
|
|
blocks.forEach((block) => {
|
|
const lines = block.split("\n");
|
|
const commandIndices = [];
|
|
lines.forEach((ln, i) => {
|
|
if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
|
|
commandIndices.push(i);
|
|
}
|
|
});
|
|
if (commandIndices.length === 0) {
|
|
return;
|
|
}
|
|
const siteFix = {
|
|
url: parseArray(lines.slice(0, commandIndices[0]).join("\n"))
|
|
};
|
|
commandIndices.forEach((commandIndex, i) => {
|
|
const command = lines[commandIndex].trim();
|
|
const valueText = lines
|
|
.slice(
|
|
commandIndex + 1,
|
|
i === commandIndices.length - 1
|
|
? lines.length
|
|
: commandIndices[i + 1]
|
|
)
|
|
.join("\n");
|
|
const prop = options.getCommandPropName(command);
|
|
if (!prop) {
|
|
return;
|
|
}
|
|
const value = options.parseCommandValue(command, valueText);
|
|
siteFix[prop] = value;
|
|
});
|
|
sites.push(siteFix);
|
|
});
|
|
return sites;
|
|
}
|
|
function getDomain(url) {
|
|
try {
|
|
return new URL(url).hostname.toLowerCase();
|
|
} catch (error) {
|
|
return url.split("/")[0].toLowerCase();
|
|
}
|
|
}
|
|
function encodeOffsets(offsets) {
|
|
return offsets
|
|
.map(([offset, length]) => {
|
|
const stringOffset = offset.toString(36);
|
|
const stringLength = length.toString(36);
|
|
return (
|
|
"0".repeat(4 - stringOffset.length) +
|
|
stringOffset +
|
|
"0".repeat(3 - stringLength.length) +
|
|
stringLength
|
|
);
|
|
})
|
|
.join("");
|
|
}
|
|
function decodeOffset(offsets, index) {
|
|
const base = (4 + 3) * index;
|
|
const offset = parseInt(offsets.substring(base + 0, base + 4), 36);
|
|
const length = parseInt(offsets.substring(base + 4, base + 4 + 3), 36);
|
|
return [offset, offset + length];
|
|
}
|
|
function indexSitesFixesConfig(text) {
|
|
const domains = {};
|
|
const domainPatterns = {};
|
|
const offsets = [];
|
|
function processBlock(recordStart, recordEnd, index) {
|
|
const block = text.substring(recordStart, recordEnd);
|
|
const lines = block.split("\n");
|
|
const commandIndices = [];
|
|
lines.forEach((ln, i) => {
|
|
if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
|
|
commandIndices.push(i);
|
|
}
|
|
});
|
|
if (commandIndices.length === 0) {
|
|
return;
|
|
}
|
|
const urls = parseArray(
|
|
lines.slice(0, commandIndices[0]).join("\n")
|
|
);
|
|
for (const url of urls) {
|
|
const domain = getDomain(url);
|
|
if (isFullyQualifiedDomain(domain)) {
|
|
if (!domains[domain]) {
|
|
domains[domain] = index;
|
|
} else if (
|
|
typeof domains[domain] === "number" &&
|
|
domains[domain] !== index
|
|
) {
|
|
domains[domain] = [domains[domain], index];
|
|
} else if (
|
|
typeof domains[domain] === "object" &&
|
|
!domains[domain].includes(index)
|
|
) {
|
|
domains[domain].push(index);
|
|
}
|
|
continue;
|
|
}
|
|
if (!domainPatterns[domain]) {
|
|
domainPatterns[domain] = index;
|
|
} else if (
|
|
typeof domainPatterns[domain] === "number" &&
|
|
domainPatterns[domain] !== index
|
|
) {
|
|
domainPatterns[domain] = [domainPatterns[domain], index];
|
|
} else if (
|
|
typeof domainPatterns[domain] === "object" &&
|
|
!domainPatterns[domain].includes(index)
|
|
) {
|
|
domainPatterns[domain].push(index);
|
|
}
|
|
}
|
|
offsets.push([recordStart, recordEnd - recordStart]);
|
|
}
|
|
let recordStart = 0;
|
|
const delimiterRegex = /^\s*={2,}\s*$/gm;
|
|
let delimiter;
|
|
let count = 0;
|
|
while ((delimiter = delimiterRegex.exec(text))) {
|
|
const nextDelimiterStart = delimiter.index;
|
|
const nextDelimiterEnd = delimiter.index + delimiter[0].length;
|
|
processBlock(recordStart, nextDelimiterStart, count);
|
|
recordStart = nextDelimiterEnd;
|
|
count++;
|
|
}
|
|
processBlock(recordStart, text.length, count);
|
|
return {
|
|
offsets: encodeOffsets(offsets),
|
|
domains,
|
|
domainPatterns,
|
|
cache: {}
|
|
};
|
|
}
|
|
function parseSiteFixConfig(text, options, recordStart, recordEnd) {
|
|
const block = text.substring(recordStart, recordEnd);
|
|
return parseSitesFixesConfig(block, options)[0];
|
|
}
|
|
function getSitesFixesFor(url, text, index, options) {
|
|
const records = [];
|
|
let recordIds = [];
|
|
const domain = getDomain(url);
|
|
for (const pattern of Object.keys(index.domainPatterns)) {
|
|
if (isURLMatched(url, pattern)) {
|
|
recordIds = recordIds.concat(index.domainPatterns[pattern]);
|
|
}
|
|
}
|
|
const labels = domain.split(".");
|
|
for (let i = 0; i < labels.length; i++) {
|
|
const substring = labels.slice(i).join(".");
|
|
if (index.domains[substring] && isURLMatched(url, substring)) {
|
|
recordIds = recordIds.concat(index.domains[substring]);
|
|
}
|
|
}
|
|
const set = new Set();
|
|
for (const id of recordIds) {
|
|
if (set.has(id)) {
|
|
continue;
|
|
}
|
|
set.add(id);
|
|
if (!index.cache[id]) {
|
|
const [start, end] = decodeOffset(index.offsets, id);
|
|
index.cache[id] = parseSiteFixConfig(text, options, start, end);
|
|
}
|
|
records.push(index.cache[id]);
|
|
}
|
|
return records;
|
|
}
|
|
|
|
const SEPERATOR = "=".repeat(32);
|
|
const backgroundPropertyLength = "background: ".length;
|
|
const textPropertyLength = "text: ".length;
|
|
const humanizeNumber = (number) => {
|
|
if (number > 3) {
|
|
return `${number}th`;
|
|
}
|
|
switch (number) {
|
|
case 0:
|
|
return "0";
|
|
case 1:
|
|
return "1st";
|
|
case 2:
|
|
return "2nd";
|
|
case 3:
|
|
return "3rd";
|
|
}
|
|
};
|
|
const isValidHexColor = (color) => {
|
|
return /^#([0-9a-fA-F]{3}){1,2}$/.test(color);
|
|
};
|
|
function ParseColorSchemeConfig(config) {
|
|
const sections = config.split(`${SEPERATOR}\n\n`);
|
|
const definedColorSchemeNames = new Set();
|
|
let lastDefinedColorSchemeName = "";
|
|
const definedColorSchemes = {
|
|
light: {},
|
|
dark: {}
|
|
};
|
|
let interrupt = false;
|
|
let error = null;
|
|
const throwError = (message) => {
|
|
if (!interrupt) {
|
|
interrupt = true;
|
|
error = message;
|
|
}
|
|
};
|
|
sections.forEach((section) => {
|
|
if (interrupt) {
|
|
return;
|
|
}
|
|
const lines = section.split("\n");
|
|
const name = lines[0];
|
|
if (!name) {
|
|
throwError("No color scheme name was found.");
|
|
return;
|
|
}
|
|
if (definedColorSchemeNames.has(name)) {
|
|
throwError(
|
|
`The color scheme name "${name}" is already defined.`
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
lastDefinedColorSchemeName &&
|
|
lastDefinedColorSchemeName !== "Default" &&
|
|
name.localeCompare(lastDefinedColorSchemeName) < 0
|
|
) {
|
|
throwError(
|
|
`The color scheme name "${name}" is not in alphabetical order.`
|
|
);
|
|
return;
|
|
}
|
|
lastDefinedColorSchemeName = name;
|
|
definedColorSchemeNames.add(name);
|
|
if (lines[1]) {
|
|
throwError(
|
|
`The second line of the color scheme "${name}" is not empty.`
|
|
);
|
|
return;
|
|
}
|
|
const checkVariant = (lineIndex, isSecondVariant) => {
|
|
const variant = lines[lineIndex];
|
|
if (!variant) {
|
|
throwError(
|
|
`The third line of the color scheme "${name}" is not defined.`
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
variant !== "LIGHT" &&
|
|
variant !== "DARK" &&
|
|
isSecondVariant &&
|
|
variant === "Light"
|
|
) {
|
|
throwError(
|
|
`The ${humanizeNumber(
|
|
lineIndex
|
|
)} line of the color scheme "${name}" is not a valid variant.`
|
|
);
|
|
return;
|
|
}
|
|
const firstProperty = lines[lineIndex + 1];
|
|
if (!firstProperty) {
|
|
throwError(
|
|
`The ${humanizeNumber(
|
|
lineIndex + 1
|
|
)} line of the color scheme "${name}" is not defined.`
|
|
);
|
|
return;
|
|
}
|
|
if (!firstProperty.startsWith("background: ")) {
|
|
throwError(
|
|
`The ${humanizeNumber(
|
|
lineIndex + 1
|
|
)} line of the color scheme "${name}" is not background-color property.`
|
|
);
|
|
return;
|
|
}
|
|
const backgroundColor = firstProperty.slice(
|
|
backgroundPropertyLength
|
|
);
|
|
if (!isValidHexColor(backgroundColor)) {
|
|
throwError(
|
|
`The ${humanizeNumber(
|
|
lineIndex + 1
|
|
)} line of the color scheme "${name}" is not a valid hex color.`
|
|
);
|
|
return;
|
|
}
|
|
const secondProperty = lines[lineIndex + 2];
|
|
if (!secondProperty) {
|
|
throwError(
|
|
`The ${humanizeNumber(
|
|
lineIndex + 2
|
|
)} line of the color scheme "${name}" is not defined.`
|
|
);
|
|
return;
|
|
}
|
|
if (!secondProperty.startsWith("text: ")) {
|
|
throwError(
|
|
`The ${humanizeNumber(
|
|
lineIndex + 2
|
|
)} line of the color scheme "${name}" is not text-color property.`
|
|
);
|
|
return;
|
|
}
|
|
const textColor = secondProperty.slice(textPropertyLength);
|
|
if (!isValidHexColor(textColor)) {
|
|
throwError(
|
|
`The ${humanizeNumber(
|
|
lineIndex + 2
|
|
)} line of the color scheme "${name}" is not a valid hex color.`
|
|
);
|
|
return;
|
|
}
|
|
return {
|
|
backgroundColor,
|
|
textColor,
|
|
variant
|
|
};
|
|
};
|
|
const firstVariant = checkVariant(2, false);
|
|
const isFirstVariantLight = firstVariant.variant === "LIGHT";
|
|
delete firstVariant.variant;
|
|
if (interrupt) {
|
|
return;
|
|
}
|
|
let secondVariant = null;
|
|
let isSecondVariantLight = false;
|
|
if (lines[6]) {
|
|
secondVariant = checkVariant(6, true);
|
|
isSecondVariantLight = secondVariant.variant === "LIGHT";
|
|
delete secondVariant.variant;
|
|
if (interrupt) {
|
|
return;
|
|
}
|
|
if (lines.length > 11 || lines[9] || lines[10]) {
|
|
throwError(
|
|
`The color scheme "${name}" doesn't end with 1 new line.`
|
|
);
|
|
return;
|
|
}
|
|
} else if (lines.length > 7) {
|
|
throwError(
|
|
`The color scheme "${name}" doesn't end with 1 new line.`
|
|
);
|
|
return;
|
|
}
|
|
if (secondVariant) {
|
|
if (isFirstVariantLight === isSecondVariantLight) {
|
|
throwError(
|
|
`The color scheme "${name}" has the same variant twice.`
|
|
);
|
|
return;
|
|
}
|
|
if (isFirstVariantLight) {
|
|
definedColorSchemes.light[name] = firstVariant;
|
|
definedColorSchemes.dark[name] = secondVariant;
|
|
} else {
|
|
definedColorSchemes.light[name] = secondVariant;
|
|
definedColorSchemes.dark[name] = firstVariant;
|
|
}
|
|
} else if (isFirstVariantLight) {
|
|
definedColorSchemes.light[name] = firstVariant;
|
|
} else {
|
|
definedColorSchemes.dark[name] = firstVariant;
|
|
}
|
|
});
|
|
return {result: definedColorSchemes, error: error};
|
|
}
|
|
|
|
function logInfo(...args) {}
|
|
function logWarn(...args) {}
|
|
|
|
var ThemeEngines = {
|
|
cssFilter: "cssFilter",
|
|
svgFilter: "svgFilter",
|
|
staticTheme: "staticTheme",
|
|
dynamicTheme: "dynamicTheme"
|
|
};
|
|
|
|
const DEFAULT_COLORS = {
|
|
darkScheme: {
|
|
background: "#181a1b",
|
|
text: "#e8e6e3"
|
|
},
|
|
lightScheme: {
|
|
background: "#dcdad7",
|
|
text: "#181a1b"
|
|
}
|
|
};
|
|
const DEFAULT_THEME = {
|
|
mode: 1,
|
|
brightness: 100,
|
|
contrast: 100,
|
|
grayscale: 0,
|
|
sepia: 0,
|
|
useFont: false,
|
|
fontFamily: isMacOS
|
|
? "Helvetica Neue"
|
|
: isWindows
|
|
? "Segoe UI"
|
|
: "Open Sans",
|
|
textStroke: 0,
|
|
engine: ThemeEngines.dynamicTheme,
|
|
stylesheet: "",
|
|
darkSchemeBackgroundColor: DEFAULT_COLORS.darkScheme.background,
|
|
darkSchemeTextColor: DEFAULT_COLORS.darkScheme.text,
|
|
lightSchemeBackgroundColor: DEFAULT_COLORS.lightScheme.background,
|
|
lightSchemeTextColor: DEFAULT_COLORS.lightScheme.text,
|
|
scrollbarColor: isMacOS ? "" : "auto",
|
|
selectionColor: "auto",
|
|
styleSystemControls: true,
|
|
lightColorScheme: "Default",
|
|
darkColorScheme: "Default"
|
|
};
|
|
const DEFAULT_COLORSCHEME = {
|
|
light: {
|
|
Default: {
|
|
backgroundColor: DEFAULT_COLORS.lightScheme.background,
|
|
textColor: DEFAULT_COLORS.lightScheme.text
|
|
}
|
|
},
|
|
dark: {
|
|
Default: {
|
|
backgroundColor: DEFAULT_COLORS.darkScheme.background,
|
|
textColor: DEFAULT_COLORS.darkScheme.text
|
|
}
|
|
}
|
|
};
|
|
const DEFAULT_SETTINGS = {
|
|
enabled: true,
|
|
fetchNews: true,
|
|
theme: DEFAULT_THEME,
|
|
presets: [],
|
|
customThemes: [],
|
|
siteList: [],
|
|
siteListEnabled: [],
|
|
applyToListedOnly: false,
|
|
changeBrowserTheme: false,
|
|
syncSettings: true,
|
|
syncSitesFixes: false,
|
|
automation: "",
|
|
time: {
|
|
activation: "18:00",
|
|
deactivation: "9:00"
|
|
},
|
|
location: {
|
|
latitude: null,
|
|
longitude: null
|
|
},
|
|
previewNewDesign: false,
|
|
enableForPDF: true,
|
|
enableForProtectedPages: false,
|
|
enableContextMenus: false
|
|
};
|
|
|
|
const CONFIG_URLs = {
|
|
darkSites: {
|
|
remote: "https://raw.githubusercontent.com/darkreader/darkreader/master/src/config/dark-sites.config",
|
|
local: "../config/dark-sites.config"
|
|
},
|
|
dynamicThemeFixes: {
|
|
remote: "https://raw.githubusercontent.com/darkreader/darkreader/master/src/config/dynamic-theme-fixes.config",
|
|
local: "../config/dynamic-theme-fixes.config"
|
|
},
|
|
inversionFixes: {
|
|
remote: "https://raw.githubusercontent.com/darkreader/darkreader/master/src/config/inversion-fixes.config",
|
|
local: "../config/inversion-fixes.config"
|
|
},
|
|
staticThemes: {
|
|
remote: "https://raw.githubusercontent.com/darkreader/darkreader/master/src/config/static-themes.config",
|
|
local: "../config/static-themes.config"
|
|
},
|
|
colorSchemes: {
|
|
remote: "https://raw.githubusercontent.com/darkreader/darkreader/master/src/config/color-schemes.drconf",
|
|
local: "../config/color-schemes.drconf"
|
|
}
|
|
};
|
|
const REMOTE_TIMEOUT_MS = getDuration({seconds: 10});
|
|
class ConfigManager {
|
|
constructor() {
|
|
this.raw = {
|
|
darkSites: null,
|
|
dynamicThemeFixes: null,
|
|
inversionFixes: null,
|
|
staticThemes: null,
|
|
colorSchemes: null
|
|
};
|
|
this.overrides = {
|
|
darkSites: null,
|
|
dynamicThemeFixes: null,
|
|
inversionFixes: null,
|
|
staticThemes: null
|
|
};
|
|
}
|
|
async loadConfig({name, local, localURL, remoteURL}) {
|
|
let $config;
|
|
const loadLocal = async () => await readText({url: localURL});
|
|
if (local) {
|
|
$config = await loadLocal();
|
|
} else {
|
|
try {
|
|
$config = await readText({
|
|
url: `${remoteURL}?nocache=${Date.now()}`,
|
|
timeout: REMOTE_TIMEOUT_MS
|
|
});
|
|
} catch (err) {
|
|
console.error(`${name} remote load error`, err);
|
|
$config = await loadLocal();
|
|
}
|
|
}
|
|
return $config;
|
|
}
|
|
async loadColorSchemes({local}) {
|
|
const $config = await this.loadConfig({
|
|
name: "Color Schemes",
|
|
local,
|
|
localURL: CONFIG_URLs.colorSchemes.local,
|
|
remoteURL: CONFIG_URLs.colorSchemes.remote
|
|
});
|
|
this.raw.colorSchemes = $config;
|
|
this.handleColorSchemes();
|
|
}
|
|
async loadDarkSites({local}) {
|
|
const sites = await this.loadConfig({
|
|
name: "Dark Sites",
|
|
local,
|
|
localURL: CONFIG_URLs.darkSites.local,
|
|
remoteURL: CONFIG_URLs.darkSites.remote
|
|
});
|
|
this.raw.darkSites = sites;
|
|
this.handleDarkSites();
|
|
}
|
|
async loadDynamicThemeFixes({local}) {
|
|
const fixes = await this.loadConfig({
|
|
name: "Dynamic Theme Fixes",
|
|
local,
|
|
localURL: CONFIG_URLs.dynamicThemeFixes.local,
|
|
remoteURL: CONFIG_URLs.dynamicThemeFixes.remote
|
|
});
|
|
this.raw.dynamicThemeFixes = fixes;
|
|
this.handleDynamicThemeFixes();
|
|
}
|
|
async loadInversionFixes({local}) {
|
|
const fixes = await this.loadConfig({
|
|
name: "Inversion Fixes",
|
|
local,
|
|
localURL: CONFIG_URLs.inversionFixes.local,
|
|
remoteURL: CONFIG_URLs.inversionFixes.remote
|
|
});
|
|
this.raw.inversionFixes = fixes;
|
|
this.handleInversionFixes();
|
|
}
|
|
async loadStaticThemes({local}) {
|
|
const themes = await this.loadConfig({
|
|
name: "Static Themes",
|
|
local,
|
|
localURL: CONFIG_URLs.staticThemes.local,
|
|
remoteURL: CONFIG_URLs.staticThemes.remote
|
|
});
|
|
this.raw.staticThemes = themes;
|
|
this.handleStaticThemes();
|
|
}
|
|
async load(config) {
|
|
await Promise.all([
|
|
this.loadColorSchemes(config),
|
|
this.loadDarkSites(config),
|
|
this.loadDynamicThemeFixes(config),
|
|
this.loadInversionFixes(config),
|
|
this.loadStaticThemes(config)
|
|
]).catch((err) => console.error("Fatality", err));
|
|
}
|
|
handleColorSchemes() {
|
|
const $config = this.raw.colorSchemes;
|
|
const {result, error} = ParseColorSchemeConfig($config);
|
|
if (error) {
|
|
this.COLOR_SCHEMES_RAW = DEFAULT_COLORSCHEME;
|
|
return;
|
|
}
|
|
this.COLOR_SCHEMES_RAW = result;
|
|
}
|
|
handleDarkSites() {
|
|
const $sites = this.overrides.darkSites || this.raw.darkSites;
|
|
this.DARK_SITES = parseArray($sites);
|
|
}
|
|
handleDynamicThemeFixes() {
|
|
const $fixes =
|
|
this.overrides.dynamicThemeFixes || this.raw.dynamicThemeFixes;
|
|
this.DYNAMIC_THEME_FIXES_INDEX = indexSitesFixesConfig($fixes);
|
|
this.DYNAMIC_THEME_FIXES_RAW = $fixes;
|
|
}
|
|
handleInversionFixes() {
|
|
const $fixes =
|
|
this.overrides.inversionFixes || this.raw.inversionFixes;
|
|
this.INVERSION_FIXES_INDEX = indexSitesFixesConfig($fixes);
|
|
this.INVERSION_FIXES_RAW = $fixes;
|
|
}
|
|
handleStaticThemes() {
|
|
const $themes =
|
|
this.overrides.staticThemes || this.raw.staticThemes;
|
|
this.STATIC_THEMES_INDEX = indexSitesFixesConfig($themes);
|
|
this.STATIC_THEMES_RAW = $themes;
|
|
}
|
|
}
|
|
|
|
function isArrayLike(items) {
|
|
return items.length != null;
|
|
}
|
|
function forEach(items, iterator) {
|
|
if (isArrayLike(items)) {
|
|
for (let i = 0, len = items.length; i < len; i++) {
|
|
iterator(items[i]);
|
|
}
|
|
} else {
|
|
for (const item of items) {
|
|
iterator(item);
|
|
}
|
|
}
|
|
}
|
|
function push(array, addition) {
|
|
forEach(addition, (a) => array.push(a));
|
|
}
|
|
|
|
function formatSitesFixesConfig(fixes, options) {
|
|
const lines = [];
|
|
fixes.forEach((fix, i) => {
|
|
push(lines, fix.url);
|
|
options.props.forEach((prop) => {
|
|
const command = options.getPropCommandName(prop);
|
|
const value = fix[prop];
|
|
if (options.shouldIgnoreProp(prop, value)) {
|
|
return;
|
|
}
|
|
lines.push("");
|
|
lines.push(command);
|
|
const formattedValue = options.formatPropValue(prop, value);
|
|
if (formattedValue) {
|
|
lines.push(formattedValue);
|
|
}
|
|
});
|
|
if (i < fixes.length - 1) {
|
|
lines.push("");
|
|
lines.push("=".repeat(32));
|
|
lines.push("");
|
|
}
|
|
});
|
|
lines.push("");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function scale(x, inLow, inHigh, outLow, outHigh) {
|
|
return ((x - inLow) * (outHigh - outLow)) / (inHigh - inLow) + outLow;
|
|
}
|
|
function clamp(x, min, max) {
|
|
return Math.min(max, Math.max(min, x));
|
|
}
|
|
function multiplyMatrices(m1, m2) {
|
|
const result = [];
|
|
for (let i = 0, len = m1.length; i < len; i++) {
|
|
result[i] = [];
|
|
for (let j = 0, len2 = m2[0].length; j < len2; j++) {
|
|
let sum = 0;
|
|
for (let k = 0, len3 = m1[0].length; k < len3; k++) {
|
|
sum += m1[i][k] * m2[k][j];
|
|
}
|
|
result[i][j] = sum;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function createFilterMatrix(config) {
|
|
let m = Matrix.identity();
|
|
if (config.sepia !== 0) {
|
|
m = multiplyMatrices(m, Matrix.sepia(config.sepia / 100));
|
|
}
|
|
if (config.grayscale !== 0) {
|
|
m = multiplyMatrices(m, Matrix.grayscale(config.grayscale / 100));
|
|
}
|
|
if (config.contrast !== 100) {
|
|
m = multiplyMatrices(m, Matrix.contrast(config.contrast / 100));
|
|
}
|
|
if (config.brightness !== 100) {
|
|
m = multiplyMatrices(m, Matrix.brightness(config.brightness / 100));
|
|
}
|
|
if (config.mode === 1) {
|
|
m = multiplyMatrices(m, Matrix.invertNHue());
|
|
}
|
|
return m;
|
|
}
|
|
function applyColorMatrix([r, g, b], matrix) {
|
|
const rgb = [[r / 255], [g / 255], [b / 255], [1], [1]];
|
|
const result = multiplyMatrices(matrix, rgb);
|
|
return [0, 1, 2].map((i) =>
|
|
clamp(Math.round(result[i][0] * 255), 0, 255)
|
|
);
|
|
}
|
|
const Matrix = {
|
|
identity() {
|
|
return [
|
|
[1, 0, 0, 0, 0],
|
|
[0, 1, 0, 0, 0],
|
|
[0, 0, 1, 0, 0],
|
|
[0, 0, 0, 1, 0],
|
|
[0, 0, 0, 0, 1]
|
|
];
|
|
},
|
|
invertNHue() {
|
|
return [
|
|
[0.333, -0.667, -0.667, 0, 1],
|
|
[-0.667, 0.333, -0.667, 0, 1],
|
|
[-0.667, -0.667, 0.333, 0, 1],
|
|
[0, 0, 0, 1, 0],
|
|
[0, 0, 0, 0, 1]
|
|
];
|
|
},
|
|
brightness(v) {
|
|
return [
|
|
[v, 0, 0, 0, 0],
|
|
[0, v, 0, 0, 0],
|
|
[0, 0, v, 0, 0],
|
|
[0, 0, 0, 1, 0],
|
|
[0, 0, 0, 0, 1]
|
|
];
|
|
},
|
|
contrast(v) {
|
|
const t = (1 - v) / 2;
|
|
return [
|
|
[v, 0, 0, 0, t],
|
|
[0, v, 0, 0, t],
|
|
[0, 0, v, 0, t],
|
|
[0, 0, 0, 1, 0],
|
|
[0, 0, 0, 0, 1]
|
|
];
|
|
},
|
|
sepia(v) {
|
|
return [
|
|
[
|
|
0.393 + 0.607 * (1 - v),
|
|
0.769 - 0.769 * (1 - v),
|
|
0.189 - 0.189 * (1 - v),
|
|
0,
|
|
0
|
|
],
|
|
[
|
|
0.349 - 0.349 * (1 - v),
|
|
0.686 + 0.314 * (1 - v),
|
|
0.168 - 0.168 * (1 - v),
|
|
0,
|
|
0
|
|
],
|
|
[
|
|
0.272 - 0.272 * (1 - v),
|
|
0.534 - 0.534 * (1 - v),
|
|
0.131 + 0.869 * (1 - v),
|
|
0,
|
|
0
|
|
],
|
|
[0, 0, 0, 1, 0],
|
|
[0, 0, 0, 0, 1]
|
|
];
|
|
},
|
|
grayscale(v) {
|
|
return [
|
|
[
|
|
0.2126 + 0.7874 * (1 - v),
|
|
0.7152 - 0.7152 * (1 - v),
|
|
0.0722 - 0.0722 * (1 - v),
|
|
0,
|
|
0
|
|
],
|
|
[
|
|
0.2126 - 0.2126 * (1 - v),
|
|
0.7152 + 0.2848 * (1 - v),
|
|
0.0722 - 0.0722 * (1 - v),
|
|
0,
|
|
0
|
|
],
|
|
[
|
|
0.2126 - 0.2126 * (1 - v),
|
|
0.7152 - 0.7152 * (1 - v),
|
|
0.0722 + 0.9278 * (1 - v),
|
|
0,
|
|
0
|
|
],
|
|
[0, 0, 0, 1, 0],
|
|
[0, 0, 0, 0, 1]
|
|
];
|
|
}
|
|
};
|
|
|
|
function createTextStyle(config) {
|
|
const lines = [];
|
|
lines.push(
|
|
'*:not(pre, pre *, code, .far, .fa, .glyphicon, [class*="vjs-"], .fab, .fa-github, .fas, .material-icons, .icofont, .typcn, mu, [class*="mu-"], .glyphicon, .icon) {'
|
|
);
|
|
if (config.useFont && config.fontFamily) {
|
|
lines.push(` font-family: ${config.fontFamily} !important;`);
|
|
}
|
|
if (config.textStroke > 0) {
|
|
lines.push(
|
|
` -webkit-text-stroke: ${config.textStroke}px !important;`
|
|
);
|
|
lines.push(` text-stroke: ${config.textStroke}px !important;`);
|
|
}
|
|
lines.push("}");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
var FilterMode;
|
|
(function (FilterMode) {
|
|
FilterMode[(FilterMode["light"] = 0)] = "light";
|
|
FilterMode[(FilterMode["dark"] = 1)] = "dark";
|
|
})(FilterMode || (FilterMode = {}));
|
|
function hasPatchForChromiumIssue501582() {
|
|
return Boolean(
|
|
isChromium &&
|
|
compareChromeVersions(chromiumVersion, "81.0.4035.0") >= 0
|
|
);
|
|
}
|
|
function createCSSFilterStyleSheet(config, url, frameURL, fixes, index) {
|
|
const filterValue = getCSSFilterValue(config);
|
|
const reverseFilterValue = "invert(100%) hue-rotate(180deg)";
|
|
return cssFilterStyleSheetTemplate(
|
|
filterValue,
|
|
reverseFilterValue,
|
|
config,
|
|
url,
|
|
frameURL,
|
|
fixes,
|
|
index
|
|
);
|
|
}
|
|
function cssFilterStyleSheetTemplate(
|
|
filterValue,
|
|
reverseFilterValue,
|
|
config,
|
|
url,
|
|
frameURL,
|
|
fixes,
|
|
index
|
|
) {
|
|
const fix = getInversionFixesFor(frameURL || url, fixes, index);
|
|
const lines = [];
|
|
lines.push("@media screen {");
|
|
if (filterValue && !frameURL) {
|
|
lines.push("");
|
|
lines.push("/* Leading rule */");
|
|
lines.push(createLeadingRule(filterValue));
|
|
}
|
|
if (config.mode === FilterMode.dark) {
|
|
lines.push("");
|
|
lines.push("/* Reverse rule */");
|
|
lines.push(createReverseRule(reverseFilterValue, fix));
|
|
}
|
|
if (config.useFont || config.textStroke > 0) {
|
|
lines.push("");
|
|
lines.push("/* Font */");
|
|
lines.push(createTextStyle(config));
|
|
}
|
|
lines.push("");
|
|
lines.push("/* Text contrast */");
|
|
lines.push("html {");
|
|
lines.push(" text-shadow: 0 0 0 !important;");
|
|
lines.push("}");
|
|
lines.push("");
|
|
lines.push("/* Full screen */");
|
|
[":-webkit-full-screen", ":-moz-full-screen", ":fullscreen"].forEach(
|
|
(fullScreen) => {
|
|
lines.push(`${fullScreen}, ${fullScreen} * {`);
|
|
lines.push(" -webkit-filter: none !important;");
|
|
lines.push(" filter: none !important;");
|
|
lines.push("}");
|
|
}
|
|
);
|
|
if (!frameURL) {
|
|
const light = [255, 255, 255];
|
|
const bgColor =
|
|
!hasPatchForChromiumIssue501582() &&
|
|
config.mode === FilterMode.dark
|
|
? applyColorMatrix(light, createFilterMatrix(config)).map(
|
|
Math.round
|
|
)
|
|
: light;
|
|
lines.push("");
|
|
lines.push("/* Page background */");
|
|
lines.push("html {");
|
|
lines.push(` background: rgb(${bgColor.join(",")}) !important;`);
|
|
lines.push("}");
|
|
}
|
|
if (fix.css && fix.css.length > 0 && config.mode === FilterMode.dark) {
|
|
lines.push("");
|
|
lines.push("/* Custom rules */");
|
|
lines.push(fix.css);
|
|
}
|
|
lines.push("");
|
|
lines.push("}");
|
|
return lines.join("\n");
|
|
}
|
|
function getCSSFilterValue(config) {
|
|
const filters = [];
|
|
if (config.mode === FilterMode.dark) {
|
|
filters.push("invert(100%) hue-rotate(180deg)");
|
|
}
|
|
if (config.brightness !== 100) {
|
|
filters.push(`brightness(${config.brightness}%)`);
|
|
}
|
|
if (config.contrast !== 100) {
|
|
filters.push(`contrast(${config.contrast}%)`);
|
|
}
|
|
if (config.grayscale !== 0) {
|
|
filters.push(`grayscale(${config.grayscale}%)`);
|
|
}
|
|
if (config.sepia !== 0) {
|
|
filters.push(`sepia(${config.sepia}%)`);
|
|
}
|
|
if (filters.length === 0) {
|
|
return null;
|
|
}
|
|
return filters.join(" ");
|
|
}
|
|
function createLeadingRule(filterValue) {
|
|
return [
|
|
"html {",
|
|
` -webkit-filter: ${filterValue} !important;`,
|
|
` filter: ${filterValue} !important;`,
|
|
"}"
|
|
].join("\n");
|
|
}
|
|
function joinSelectors(selectors) {
|
|
return selectors.map((s) => s.replace(/\,$/, "")).join(",\n");
|
|
}
|
|
function createReverseRule(reverseFilterValue, fix) {
|
|
const lines = [];
|
|
if (fix.invert.length > 0) {
|
|
lines.push(`${joinSelectors(fix.invert)} {`);
|
|
lines.push(` -webkit-filter: ${reverseFilterValue} !important;`);
|
|
lines.push(` filter: ${reverseFilterValue} !important;`);
|
|
lines.push("}");
|
|
}
|
|
if (fix.noinvert.length > 0) {
|
|
lines.push(`${joinSelectors(fix.noinvert)} {`);
|
|
lines.push(" -webkit-filter: none !important;");
|
|
lines.push(" filter: none !important;");
|
|
lines.push("}");
|
|
}
|
|
if (fix.removebg.length > 0) {
|
|
lines.push(`${joinSelectors(fix.removebg)} {`);
|
|
lines.push(" background: white !important;");
|
|
lines.push("}");
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function getInversionFixesFor(url, fixes, index) {
|
|
const inversionFixes = getSitesFixesFor(url, fixes, index, {
|
|
commands: Object.keys(inversionFixesCommands),
|
|
getCommandPropName: (command) => inversionFixesCommands[command],
|
|
parseCommandValue: (command, value) => {
|
|
if (command === "CSS") {
|
|
return value.trim();
|
|
}
|
|
return parseArray(value);
|
|
}
|
|
});
|
|
const common = {
|
|
url: inversionFixes[0].url,
|
|
invert: inversionFixes[0].invert || [],
|
|
noinvert: inversionFixes[0].noinvert || [],
|
|
removebg: inversionFixes[0].removebg || [],
|
|
css: inversionFixes[0].css || ""
|
|
};
|
|
if (url) {
|
|
const matches = inversionFixes
|
|
.slice(1)
|
|
.filter((s) => isURLInList(url, s.url))
|
|
.sort((a, b) => b.url[0].length - a.url[0].length);
|
|
if (matches.length > 0) {
|
|
const found = matches[0];
|
|
return {
|
|
url: found.url,
|
|
invert: common.invert.concat(found.invert || []),
|
|
noinvert: common.noinvert.concat(found.noinvert || []),
|
|
removebg: common.removebg.concat(found.removebg || []),
|
|
css: [common.css, found.css].filter((s) => s).join("\n")
|
|
};
|
|
}
|
|
}
|
|
return common;
|
|
}
|
|
const inversionFixesCommands = {
|
|
"INVERT": "invert",
|
|
"NO INVERT": "noinvert",
|
|
"REMOVE BG": "removebg",
|
|
"CSS": "css"
|
|
};
|
|
function parseInversionFixes(text) {
|
|
return parseSitesFixesConfig(text, {
|
|
commands: Object.keys(inversionFixesCommands),
|
|
getCommandPropName: (command) => inversionFixesCommands[command],
|
|
parseCommandValue: (command, value) => {
|
|
if (command === "CSS") {
|
|
return value.trim();
|
|
}
|
|
return parseArray(value);
|
|
}
|
|
});
|
|
}
|
|
function formatInversionFixes(inversionFixes) {
|
|
const fixes = inversionFixes
|
|
.slice()
|
|
.sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));
|
|
return formatSitesFixesConfig(fixes, {
|
|
props: Object.values(inversionFixesCommands),
|
|
getPropCommandName: (prop) =>
|
|
Object.entries(inversionFixesCommands).find(
|
|
([, p]) => p === prop
|
|
)[0],
|
|
formatPropValue: (prop, value) => {
|
|
if (prop === "css") {
|
|
return value.trim().replace(/\n+/g, "\n");
|
|
}
|
|
return formatArray(value).trim();
|
|
},
|
|
shouldIgnoreProp: (prop, value) => {
|
|
if (prop === "css") {
|
|
return !value;
|
|
}
|
|
return !(Array.isArray(value) && value.length > 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
const dynamicThemeFixesCommands = {
|
|
"INVERT": "invert",
|
|
"CSS": "css",
|
|
"IGNORE INLINE STYLE": "ignoreInlineStyle",
|
|
"IGNORE IMAGE ANALYSIS": "ignoreImageAnalysis"
|
|
};
|
|
function parseDynamicThemeFixes(text) {
|
|
return parseSitesFixesConfig(text, {
|
|
commands: Object.keys(dynamicThemeFixesCommands),
|
|
getCommandPropName: (command) => dynamicThemeFixesCommands[command],
|
|
parseCommandValue: (command, value) => {
|
|
if (command === "CSS") {
|
|
return value.trim();
|
|
}
|
|
return parseArray(value);
|
|
}
|
|
});
|
|
}
|
|
function formatDynamicThemeFixes(dynamicThemeFixes) {
|
|
const fixes = dynamicThemeFixes
|
|
.slice()
|
|
.sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));
|
|
return formatSitesFixesConfig(fixes, {
|
|
props: Object.values(dynamicThemeFixesCommands),
|
|
getPropCommandName: (prop) =>
|
|
Object.entries(dynamicThemeFixesCommands).find(
|
|
([, p]) => p === prop
|
|
)[0],
|
|
formatPropValue: (prop, value) => {
|
|
if (prop === "css") {
|
|
return value.trim().replace(/\n+/g, "\n");
|
|
}
|
|
return formatArray(value).trim();
|
|
},
|
|
shouldIgnoreProp: (prop, value) => {
|
|
if (prop === "css") {
|
|
return !value;
|
|
}
|
|
return !(Array.isArray(value) && value.length > 0);
|
|
}
|
|
});
|
|
}
|
|
function getDynamicThemeFixesFor(
|
|
url,
|
|
frameURL,
|
|
text,
|
|
index,
|
|
enabledForPDF
|
|
) {
|
|
const fixes = getSitesFixesFor(frameURL || url, text, index, {
|
|
commands: Object.keys(dynamicThemeFixesCommands),
|
|
getCommandPropName: (command) => dynamicThemeFixesCommands[command],
|
|
parseCommandValue: (command, value) => {
|
|
if (command === "CSS") {
|
|
return value.trim();
|
|
}
|
|
return parseArray(value);
|
|
}
|
|
});
|
|
if (fixes.length === 0 || fixes[0].url[0] !== "*") {
|
|
return null;
|
|
}
|
|
const genericFix = fixes[0];
|
|
const common = {
|
|
url: genericFix.url,
|
|
invert: genericFix.invert || [],
|
|
css: genericFix.css || "",
|
|
ignoreInlineStyle: genericFix.ignoreInlineStyle || [],
|
|
ignoreImageAnalysis: genericFix.ignoreImageAnalysis || []
|
|
};
|
|
if (enabledForPDF) {
|
|
common.css +=
|
|
'\nembed[type="application/pdf"] { filter: invert(100%) contrast(90%); }';
|
|
}
|
|
const sortedBySpecificity = fixes
|
|
.slice(1)
|
|
.map((theme) => {
|
|
return {
|
|
specificity: isURLInList(frameURL || url, theme.url)
|
|
? theme.url[0].length
|
|
: 0,
|
|
theme
|
|
};
|
|
})
|
|
.filter(({specificity}) => specificity > 0)
|
|
.sort((a, b) => b.specificity - a.specificity);
|
|
if (sortedBySpecificity.length === 0) {
|
|
return common;
|
|
}
|
|
const match = sortedBySpecificity[0].theme;
|
|
return {
|
|
url: match.url,
|
|
invert: common.invert.concat(match.invert || []),
|
|
css: [common.css, match.css].filter((s) => s).join("\n"),
|
|
ignoreInlineStyle: common.ignoreInlineStyle.concat(
|
|
match.ignoreInlineStyle || []
|
|
),
|
|
ignoreImageAnalysis: common.ignoreImageAnalysis.concat(
|
|
match.ignoreImageAnalysis || []
|
|
)
|
|
};
|
|
}
|
|
|
|
const darkTheme = {
|
|
neutralBg: [16, 20, 23],
|
|
neutralText: [167, 158, 139],
|
|
redBg: [64, 12, 32],
|
|
redText: [247, 142, 102],
|
|
greenBg: [32, 64, 48],
|
|
greenText: [128, 204, 148],
|
|
blueBg: [32, 48, 64],
|
|
blueText: [128, 182, 204],
|
|
fadeBg: [16, 20, 23, 0.5],
|
|
fadeText: [167, 158, 139, 0.5]
|
|
};
|
|
const lightTheme = {
|
|
neutralBg: [255, 242, 228],
|
|
neutralText: [0, 0, 0],
|
|
redBg: [255, 85, 170],
|
|
redText: [140, 14, 48],
|
|
greenBg: [192, 255, 170],
|
|
greenText: [0, 128, 0],
|
|
blueBg: [173, 215, 229],
|
|
blueText: [28, 16, 171],
|
|
fadeBg: [0, 0, 0, 0.5],
|
|
fadeText: [0, 0, 0, 0.5]
|
|
};
|
|
function rgb([r, g, b, a]) {
|
|
if (typeof a === "number") {
|
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
}
|
|
return `rgb(${r}, ${g}, ${b})`;
|
|
}
|
|
function mix(color1, color2, t) {
|
|
return color1.map((c, i) => Math.round(c * (1 - t) + color2[i] * t));
|
|
}
|
|
function createStaticStylesheet(
|
|
config,
|
|
url,
|
|
frameURL,
|
|
staticThemes,
|
|
staticThemesIndex
|
|
) {
|
|
const srcTheme = config.mode === 1 ? darkTheme : lightTheme;
|
|
const theme = Object.entries(srcTheme).reduce((t, [prop, color]) => {
|
|
const [r, g, b, a] = color;
|
|
t[prop] = applyColorMatrix(
|
|
[r, g, b],
|
|
createFilterMatrix({...config, mode: 0})
|
|
);
|
|
if (a !== undefined) {
|
|
t[prop].push(a);
|
|
}
|
|
return t;
|
|
}, {});
|
|
const commonTheme = getCommonTheme(staticThemes, staticThemesIndex);
|
|
const siteTheme = getThemeFor(
|
|
frameURL || url,
|
|
staticThemes,
|
|
staticThemesIndex
|
|
);
|
|
const lines = [];
|
|
if (!siteTheme || !siteTheme.noCommon) {
|
|
lines.push("/* Common theme */");
|
|
lines.push(...ruleGenerators.map((gen) => gen(commonTheme, theme)));
|
|
}
|
|
if (siteTheme) {
|
|
lines.push(`/* Theme for ${siteTheme.url.join(" ")} */`);
|
|
lines.push(...ruleGenerators.map((gen) => gen(siteTheme, theme)));
|
|
}
|
|
if (config.useFont || config.textStroke > 0) {
|
|
lines.push("/* Font */");
|
|
lines.push(createTextStyle(config));
|
|
}
|
|
return lines.filter((ln) => ln).join("\n");
|
|
}
|
|
function createRuleGen(
|
|
getSelectors,
|
|
generateDeclarations,
|
|
modifySelector = (s) => s
|
|
) {
|
|
return (siteTheme, themeColors) => {
|
|
const selectors = getSelectors(siteTheme);
|
|
if (selectors == null || selectors.length === 0) {
|
|
return null;
|
|
}
|
|
const lines = [];
|
|
selectors.forEach((s, i) => {
|
|
let ln = modifySelector(s);
|
|
if (i < selectors.length - 1) {
|
|
ln += ",";
|
|
} else {
|
|
ln += " {";
|
|
}
|
|
lines.push(ln);
|
|
});
|
|
const declarations = generateDeclarations(themeColors);
|
|
declarations.forEach((d) => lines.push(` ${d} !important;`));
|
|
lines.push("}");
|
|
return lines.join("\n");
|
|
};
|
|
}
|
|
const mx = {
|
|
bg: {
|
|
hover: 0.075,
|
|
active: 0.1
|
|
},
|
|
fg: {
|
|
hover: 0.25,
|
|
active: 0.5
|
|
},
|
|
border: 0.5
|
|
};
|
|
const ruleGenerators = [
|
|
createRuleGen(
|
|
(t) => t.neutralBg,
|
|
(t) => [`background-color: ${rgb(t.neutralBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.neutralBgActive,
|
|
(t) => [`background-color: ${rgb(t.neutralBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.neutralBgActive,
|
|
(t) => [
|
|
`background-color: ${rgb(
|
|
mix(t.neutralBg, [255, 255, 255], mx.bg.hover)
|
|
)}`
|
|
],
|
|
(s) => `${s}:hover`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.neutralBgActive,
|
|
(t) => [
|
|
`background-color: ${rgb(
|
|
mix(t.neutralBg, [255, 255, 255], mx.bg.active)
|
|
)}`
|
|
],
|
|
(s) => `${s}:active, ${s}:focus`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.neutralText,
|
|
(t) => [`color: ${rgb(t.neutralText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.neutralTextActive,
|
|
(t) => [`color: ${rgb(t.neutralText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.neutralTextActive,
|
|
(t) => [
|
|
`color: ${rgb(
|
|
mix(t.neutralText, [255, 255, 255], mx.fg.hover)
|
|
)}`
|
|
],
|
|
(s) => `${s}:hover`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.neutralTextActive,
|
|
(t) => [
|
|
`color: ${rgb(
|
|
mix(t.neutralText, [255, 255, 255], mx.fg.active)
|
|
)}`
|
|
],
|
|
(s) => `${s}:active, ${s}:focus`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.neutralBorder,
|
|
(t) => [
|
|
`border-color: ${rgb(
|
|
mix(t.neutralBg, t.neutralText, mx.border)
|
|
)}`
|
|
]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redBg,
|
|
(t) => [`background-color: ${rgb(t.redBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redBgActive,
|
|
(t) => [`background-color: ${rgb(t.redBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redBgActive,
|
|
(t) => [
|
|
`background-color: ${rgb(
|
|
mix(t.redBg, [255, 0, 64], mx.bg.hover)
|
|
)}`
|
|
],
|
|
(s) => `${s}:hover`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redBgActive,
|
|
(t) => [
|
|
`background-color: ${rgb(
|
|
mix(t.redBg, [255, 0, 64], mx.bg.active)
|
|
)}`
|
|
],
|
|
(s) => `${s}:active, ${s}:focus`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redText,
|
|
(t) => [`color: ${rgb(t.redText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redTextActive,
|
|
(t) => [`color: ${rgb(t.redText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redTextActive,
|
|
(t) => [
|
|
`color: ${rgb(mix(t.redText, [255, 255, 0], mx.fg.hover))}`
|
|
],
|
|
(s) => `${s}:hover`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redTextActive,
|
|
(t) => [
|
|
`color: ${rgb(mix(t.redText, [255, 255, 0], mx.fg.active))}`
|
|
],
|
|
(s) => `${s}:active, ${s}:focus`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.redBorder,
|
|
(t) => [`border-color: ${rgb(mix(t.redBg, t.redText, mx.border))}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenBg,
|
|
(t) => [`background-color: ${rgb(t.greenBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenBgActive,
|
|
(t) => [`background-color: ${rgb(t.greenBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenBgActive,
|
|
(t) => [
|
|
`background-color: ${rgb(
|
|
mix(t.greenBg, [128, 255, 182], mx.bg.hover)
|
|
)}`
|
|
],
|
|
(s) => `${s}:hover`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenBgActive,
|
|
(t) => [
|
|
`background-color: ${rgb(
|
|
mix(t.greenBg, [128, 255, 182], mx.bg.active)
|
|
)}`
|
|
],
|
|
(s) => `${s}:active, ${s}:focus`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenText,
|
|
(t) => [`color: ${rgb(t.greenText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenTextActive,
|
|
(t) => [`color: ${rgb(t.greenText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenTextActive,
|
|
(t) => [
|
|
`color: ${rgb(mix(t.greenText, [182, 255, 224], mx.fg.hover))}`
|
|
],
|
|
(s) => `${s}:hover`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenTextActive,
|
|
(t) => [
|
|
`color: ${rgb(mix(t.greenText, [182, 255, 224], mx.fg.active))}`
|
|
],
|
|
(s) => `${s}:active, ${s}:focus`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.greenBorder,
|
|
(t) => [
|
|
`border-color: ${rgb(mix(t.greenBg, t.greenText, mx.border))}`
|
|
]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueBg,
|
|
(t) => [`background-color: ${rgb(t.blueBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueBgActive,
|
|
(t) => [`background-color: ${rgb(t.blueBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueBgActive,
|
|
(t) => [
|
|
`background-color: ${rgb(
|
|
mix(t.blueBg, [0, 128, 255], mx.bg.hover)
|
|
)}`
|
|
],
|
|
(s) => `${s}:hover`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueBgActive,
|
|
(t) => [
|
|
`background-color: ${rgb(
|
|
mix(t.blueBg, [0, 128, 255], mx.bg.active)
|
|
)}`
|
|
],
|
|
(s) => `${s}:active, ${s}:focus`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueText,
|
|
(t) => [`color: ${rgb(t.blueText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueTextActive,
|
|
(t) => [`color: ${rgb(t.blueText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueTextActive,
|
|
(t) => [
|
|
`color: ${rgb(mix(t.blueText, [182, 224, 255], mx.fg.hover))}`
|
|
],
|
|
(s) => `${s}:hover`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueTextActive,
|
|
(t) => [
|
|
`color: ${rgb(mix(t.blueText, [182, 224, 255], mx.fg.active))}`
|
|
],
|
|
(s) => `${s}:active, ${s}:focus`
|
|
),
|
|
createRuleGen(
|
|
(t) => t.blueBorder,
|
|
(t) => [
|
|
`border-color: ${rgb(mix(t.blueBg, t.blueText, mx.border))}`
|
|
]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.fadeBg,
|
|
(t) => [`background-color: ${rgb(t.fadeBg)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.fadeText,
|
|
(t) => [`color: ${rgb(t.fadeText)}`]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.transparentBg,
|
|
() => ["background-color: transparent"]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.noImage,
|
|
() => ["background-image: none"]
|
|
),
|
|
createRuleGen(
|
|
(t) => t.invert,
|
|
() => ["filter: invert(100%) hue-rotate(180deg)"]
|
|
)
|
|
];
|
|
const staticThemeCommands = {
|
|
"NO COMMON": "noCommon",
|
|
"NEUTRAL BG": "neutralBg",
|
|
"NEUTRAL BG ACTIVE": "neutralBgActive",
|
|
"NEUTRAL TEXT": "neutralText",
|
|
"NEUTRAL TEXT ACTIVE": "neutralTextActive",
|
|
"NEUTRAL BORDER": "neutralBorder",
|
|
"RED BG": "redBg",
|
|
"RED BG ACTIVE": "redBgActive",
|
|
"RED TEXT": "redText",
|
|
"RED TEXT ACTIVE": "redTextActive",
|
|
"RED BORDER": "redBorder",
|
|
"GREEN BG": "greenBg",
|
|
"GREEN BG ACTIVE": "greenBgActive",
|
|
"GREEN TEXT": "greenText",
|
|
"GREEN TEXT ACTIVE": "greenTextActive",
|
|
"GREEN BORDER": "greenBorder",
|
|
"BLUE BG": "blueBg",
|
|
"BLUE BG ACTIVE": "blueBgActive",
|
|
"BLUE TEXT": "blueText",
|
|
"BLUE TEXT ACTIVE": "blueTextActive",
|
|
"BLUE BORDER": "blueBorder",
|
|
"FADE BG": "fadeBg",
|
|
"FADE TEXT": "fadeText",
|
|
"TRANSPARENT BG": "transparentBg",
|
|
"NO IMAGE": "noImage",
|
|
"INVERT": "invert"
|
|
};
|
|
function parseStaticThemes($themes) {
|
|
return parseSitesFixesConfig($themes, {
|
|
commands: Object.keys(staticThemeCommands),
|
|
getCommandPropName: (command) => staticThemeCommands[command],
|
|
parseCommandValue: (command, value) => {
|
|
if (command === "NO COMMON") {
|
|
return true;
|
|
}
|
|
return parseArray(value);
|
|
}
|
|
});
|
|
}
|
|
function camelCaseToUpperCase(text) {
|
|
return text.replace(/([a-z])([A-Z])/g, "$1 $2").toUpperCase();
|
|
}
|
|
function formatStaticThemes(staticThemes) {
|
|
const themes = staticThemes
|
|
.slice()
|
|
.sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));
|
|
return formatSitesFixesConfig(themes, {
|
|
props: Object.values(staticThemeCommands),
|
|
getPropCommandName: camelCaseToUpperCase,
|
|
formatPropValue: (prop, value) => {
|
|
if (prop === "noCommon") {
|
|
return "";
|
|
}
|
|
return formatArray(value).trim();
|
|
},
|
|
shouldIgnoreProp: (prop, value) => {
|
|
if (prop === "noCommon") {
|
|
return !value;
|
|
}
|
|
return !(Array.isArray(value) && value.length > 0);
|
|
}
|
|
});
|
|
}
|
|
function getCommonTheme(staticThemes, staticThemesIndex) {
|
|
const length = parseInt(
|
|
staticThemesIndex.offsets.substring(4, 4 + 3),
|
|
36
|
|
);
|
|
const staticThemeText = staticThemes.substring(0, length);
|
|
return parseStaticThemes(staticThemeText)[0];
|
|
}
|
|
function getThemeFor(url, staticThemes, staticThemesIndex) {
|
|
const themes = getSitesFixesFor(url, staticThemes, staticThemesIndex, {
|
|
commands: Object.keys(staticThemeCommands),
|
|
getCommandPropName: (command) => staticThemeCommands[command],
|
|
parseCommandValue: (command, value) => {
|
|
if (command === "NO COMMON") {
|
|
return true;
|
|
}
|
|
return parseArray(value);
|
|
}
|
|
});
|
|
const sortedBySpecificity = themes
|
|
.slice(1)
|
|
.map((theme) => {
|
|
return {
|
|
specificity: isURLInList(url, theme.url)
|
|
? theme.url[0].length
|
|
: 0,
|
|
theme
|
|
};
|
|
})
|
|
.filter(({specificity}) => specificity > 0)
|
|
.sort((a, b) => b.specificity - a.specificity);
|
|
if (sortedBySpecificity.length === 0) {
|
|
return null;
|
|
}
|
|
return sortedBySpecificity[0].theme;
|
|
}
|
|
|
|
class PersistentStorageWrapper {
|
|
constructor() {
|
|
this.cache = {};
|
|
this.dataIsMigrated = false;
|
|
}
|
|
setDataIsMigratedForTesting(value) {}
|
|
async migrateFromLocalStorage() {
|
|
if (typeof localStorage === "undefined") {
|
|
this.dataIsMigrated = true;
|
|
return;
|
|
}
|
|
return new Promise((resolve) => {
|
|
chrome.storage.local.get(
|
|
[
|
|
DevTools.KEY_DYNAMIC,
|
|
DevTools.KEY_FILTER,
|
|
DevTools.KEY_STATIC
|
|
],
|
|
(data) => {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(
|
|
"DevTools failed to migrate data",
|
|
chrome.runtime.lastError
|
|
);
|
|
resolve();
|
|
}
|
|
if (
|
|
data[DevTools.KEY_DYNAMIC] ||
|
|
data[DevTools.KEY_FILTER] ||
|
|
data[DevTools.KEY_STATIC]
|
|
) {
|
|
this.dataIsMigrated = true;
|
|
resolve();
|
|
return;
|
|
}
|
|
this.cache = {
|
|
[DevTools.KEY_DYNAMIC]: localStorage.getItem(
|
|
DevTools.KEY_DYNAMIC
|
|
),
|
|
[DevTools.KEY_FILTER]: localStorage.getItem(
|
|
DevTools.KEY_FILTER
|
|
),
|
|
[DevTools.KEY_STATIC]: localStorage.getItem(
|
|
DevTools.KEY_STATIC
|
|
)
|
|
};
|
|
chrome.storage.local.set(this.cache, () => {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(
|
|
"DevTools failed to migrate data",
|
|
chrome.runtime.lastError
|
|
);
|
|
resolve();
|
|
}
|
|
this.dataIsMigrated = true;
|
|
localStorage.removeItem(DevTools.KEY_DYNAMIC);
|
|
localStorage.removeItem(DevTools.KEY_FILTER);
|
|
localStorage.removeItem(DevTools.KEY_STATIC);
|
|
resolve();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
}
|
|
async get(key) {
|
|
if (!this.dataIsMigrated) {
|
|
await this.migrateFromLocalStorage();
|
|
}
|
|
if (key in this.cache) {
|
|
return this.cache[key];
|
|
}
|
|
return new Promise((resolve) => {
|
|
chrome.storage.local.get(key, (result) => {
|
|
if (key in this.cache) {
|
|
resolve(this.cache[key]);
|
|
return;
|
|
}
|
|
if (chrome.runtime.lastError) {
|
|
console.error(
|
|
"Failed to query DevTools data",
|
|
chrome.runtime.lastError
|
|
);
|
|
resolve(null);
|
|
return;
|
|
}
|
|
this.cache[key] = result.key;
|
|
resolve(result.key);
|
|
});
|
|
});
|
|
}
|
|
set(key, value) {
|
|
this.cache[key] = value;
|
|
chrome.storage.local.set({[key]: value}, () => {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(
|
|
"Failed to write DevTools data",
|
|
chrome.runtime.lastError
|
|
);
|
|
}
|
|
});
|
|
}
|
|
remove(key) {
|
|
this.cache[key] = undefined;
|
|
chrome.storage.local.remove(key, () => {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(
|
|
"Failed to delete DevTools data",
|
|
chrome.runtime.lastError
|
|
);
|
|
}
|
|
});
|
|
}
|
|
async has(key) {
|
|
return Boolean(await this.get(key));
|
|
}
|
|
}
|
|
class LocalStorageWrapper {
|
|
setDataIsMigratedForTesting() {}
|
|
async get(key) {
|
|
try {
|
|
return localStorage.getItem(key);
|
|
} catch (err) {
|
|
console.error(err);
|
|
return null;
|
|
}
|
|
}
|
|
set(key, value) {
|
|
try {
|
|
localStorage.setItem(key, value);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
remove(key) {
|
|
try {
|
|
localStorage.removeItem(key);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
async has(key) {
|
|
try {
|
|
return localStorage.getItem(key) != null;
|
|
} catch (err) {
|
|
console.error(err);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
class TempStorage {
|
|
constructor() {
|
|
this.map = new Map();
|
|
}
|
|
setDataIsMigratedForTesting() {}
|
|
async get(key) {
|
|
return this.map.get(key);
|
|
}
|
|
set(key, value) {
|
|
this.map.set(key, value);
|
|
}
|
|
remove(key) {
|
|
this.map.delete(key);
|
|
}
|
|
async has(key) {
|
|
return this.map.has(key);
|
|
}
|
|
}
|
|
class DevTools {
|
|
constructor(config, onChange) {
|
|
if (
|
|
typeof chrome.storage.local !== "undefined" &&
|
|
chrome.storage.local !== null
|
|
) {
|
|
this.store = new PersistentStorageWrapper();
|
|
} else if (
|
|
typeof localStorage !== "undefined" &&
|
|
localStorage != null
|
|
) {
|
|
this.store = new LocalStorageWrapper();
|
|
} else {
|
|
this.store = new TempStorage();
|
|
}
|
|
this.config = config;
|
|
this.loadConfigOverrides();
|
|
this.onChange = onChange;
|
|
}
|
|
setDataIsMigratedForTesting(value) {
|
|
this.store.setDataIsMigratedForTesting(value);
|
|
}
|
|
async loadConfigOverrides() {
|
|
this.config.overrides.dynamicThemeFixes =
|
|
(await this.getSavedDynamicThemeFixes()) || null;
|
|
this.config.overrides.inversionFixes =
|
|
(await this.getSavedInversionFixes()) || null;
|
|
this.config.overrides.staticThemes =
|
|
(await this.getSavedStaticThemes()) || null;
|
|
}
|
|
async getSavedDynamicThemeFixes() {
|
|
return this.store.get(DevTools.KEY_DYNAMIC);
|
|
}
|
|
saveDynamicThemeFixes(text) {
|
|
this.store.set(DevTools.KEY_DYNAMIC, text);
|
|
}
|
|
async hasCustomDynamicThemeFixes() {
|
|
return this.store.has(DevTools.KEY_DYNAMIC);
|
|
}
|
|
async getDynamicThemeFixesText() {
|
|
const $fixes = await this.getSavedDynamicThemeFixes();
|
|
const fixes = $fixes
|
|
? parseDynamicThemeFixes($fixes)
|
|
: parseDynamicThemeFixes(this.config.DYNAMIC_THEME_FIXES_RAW);
|
|
return formatDynamicThemeFixes(fixes);
|
|
}
|
|
resetDynamicThemeFixes() {
|
|
this.store.remove(DevTools.KEY_DYNAMIC);
|
|
this.config.overrides.dynamicThemeFixes = null;
|
|
this.config.handleDynamicThemeFixes();
|
|
this.onChange();
|
|
}
|
|
applyDynamicThemeFixes(text) {
|
|
try {
|
|
const formatted = formatDynamicThemeFixes(
|
|
parseDynamicThemeFixes(text)
|
|
);
|
|
this.config.overrides.dynamicThemeFixes = formatted;
|
|
this.config.handleDynamicThemeFixes();
|
|
this.saveDynamicThemeFixes(formatted);
|
|
this.onChange();
|
|
return null;
|
|
} catch (err) {
|
|
return err;
|
|
}
|
|
}
|
|
async getSavedInversionFixes() {
|
|
return this.store.get(DevTools.KEY_FILTER);
|
|
}
|
|
saveInversionFixes(text) {
|
|
this.store.set(DevTools.KEY_FILTER, text);
|
|
}
|
|
async hasCustomFilterFixes() {
|
|
return this.store.has(DevTools.KEY_FILTER);
|
|
}
|
|
async getInversionFixesText() {
|
|
const $fixes = await this.getSavedInversionFixes();
|
|
const fixes = $fixes
|
|
? parseInversionFixes($fixes)
|
|
: parseInversionFixes(this.config.INVERSION_FIXES_RAW);
|
|
return formatInversionFixes(fixes);
|
|
}
|
|
resetInversionFixes() {
|
|
this.store.remove(DevTools.KEY_FILTER);
|
|
this.config.overrides.inversionFixes = null;
|
|
this.config.handleInversionFixes();
|
|
this.onChange();
|
|
}
|
|
applyInversionFixes(text) {
|
|
try {
|
|
const formatted = formatInversionFixes(
|
|
parseInversionFixes(text)
|
|
);
|
|
this.config.overrides.inversionFixes = formatted;
|
|
this.config.handleInversionFixes();
|
|
this.saveInversionFixes(formatted);
|
|
this.onChange();
|
|
return null;
|
|
} catch (err) {
|
|
return err;
|
|
}
|
|
}
|
|
async getSavedStaticThemes() {
|
|
return this.store.get(DevTools.KEY_STATIC);
|
|
}
|
|
saveStaticThemes(text) {
|
|
this.store.set(DevTools.KEY_STATIC, text);
|
|
}
|
|
async hasCustomStaticFixes() {
|
|
return this.store.has(DevTools.KEY_STATIC);
|
|
}
|
|
async getStaticThemesText() {
|
|
const $themes = await this.getSavedStaticThemes();
|
|
const themes = $themes
|
|
? parseStaticThemes($themes)
|
|
: parseStaticThemes(this.config.STATIC_THEMES_RAW);
|
|
return formatStaticThemes(themes);
|
|
}
|
|
resetStaticThemes() {
|
|
this.store.remove(DevTools.KEY_STATIC);
|
|
this.config.overrides.staticThemes = null;
|
|
this.config.handleStaticThemes();
|
|
this.onChange();
|
|
}
|
|
applyStaticThemes(text) {
|
|
try {
|
|
const formatted = formatStaticThemes(parseStaticThemes(text));
|
|
this.config.overrides.staticThemes = formatted;
|
|
this.config.handleStaticThemes();
|
|
this.saveStaticThemes(formatted);
|
|
this.onChange();
|
|
return null;
|
|
} catch (err) {
|
|
return err;
|
|
}
|
|
}
|
|
}
|
|
DevTools.KEY_DYNAMIC = "dev_dynamic_theme_fixes";
|
|
DevTools.KEY_FILTER = "dev_inversion_fixes";
|
|
DevTools.KEY_STATIC = "dev_static_themes";
|
|
|
|
const ICON_PATHS = {
|
|
active_19: "../icons/dr_active_19.png",
|
|
active_38: "../icons/dr_active_38.png",
|
|
inactive_19: "../icons/dr_inactive_19.png",
|
|
inactive_38: "../icons/dr_inactive_38.png"
|
|
};
|
|
class IconManager {
|
|
setActive() {
|
|
if (!chrome.browserAction.setIcon || isThunderbird) {
|
|
return;
|
|
}
|
|
chrome.browserAction.setIcon({
|
|
path: {
|
|
19: ICON_PATHS.active_19,
|
|
38: ICON_PATHS.active_38
|
|
}
|
|
});
|
|
}
|
|
setInactive() {
|
|
if (!chrome.browserAction.setIcon || isThunderbird) {
|
|
return;
|
|
}
|
|
chrome.browserAction.setIcon({
|
|
path: {
|
|
19: ICON_PATHS.inactive_19,
|
|
38: ICON_PATHS.inactive_38
|
|
}
|
|
});
|
|
}
|
|
showImportantBadge() {
|
|
chrome.browserAction.setBadgeBackgroundColor({color: "#e96c4c"});
|
|
chrome.browserAction.setBadgeText({text: "!"});
|
|
}
|
|
hideBadge() {
|
|
chrome.browserAction.setBadgeText({text: ""});
|
|
}
|
|
}
|
|
|
|
const MessageType = {
|
|
UI_GET_DATA: "ui-get-data",
|
|
UI_GET_ACTIVE_TAB_INFO: "ui-get-active-tab-info",
|
|
UI_SUBSCRIBE_TO_CHANGES: "ui-subscribe-to-changes",
|
|
UI_UNSUBSCRIBE_FROM_CHANGES: "ui-unsubscribe-from-changes",
|
|
UI_CHANGE_SETTINGS: "ui-change-settings",
|
|
UI_SET_THEME: "ui-set-theme",
|
|
UI_SET_SHORTCUT: "ui-set-shortcut",
|
|
UI_TOGGLE_URL: "ui-toggle-url",
|
|
UI_MARK_NEWS_AS_READ: "ui-mark-news-as-read",
|
|
UI_LOAD_CONFIG: "ui-load-config",
|
|
UI_APPLY_DEV_DYNAMIC_THEME_FIXES: "ui-apply-dev-dynamic-theme-fixes",
|
|
UI_RESET_DEV_DYNAMIC_THEME_FIXES: "ui-reset-dev-dynamic-theme-fixes",
|
|
UI_APPLY_DEV_INVERSION_FIXES: "ui-apply-dev-inversion-fixes",
|
|
UI_RESET_DEV_INVERSION_FIXES: "ui-reset-dev-inversion-fixes",
|
|
UI_APPLY_DEV_STATIC_THEMES: "ui-apply-dev-static-themes",
|
|
UI_RESET_DEV_STATIC_THEMES: "ui-reset-dev-static-themes",
|
|
UI_SAVE_FILE: "ui-save-file",
|
|
UI_REQUEST_EXPORT_CSS: "ui-request-export-css",
|
|
BG_CHANGES: "bg-changes",
|
|
BG_ADD_CSS_FILTER: "bg-add-css-filter",
|
|
BG_ADD_STATIC_THEME: "bg-add-static-theme",
|
|
BG_ADD_SVG_FILTER: "bg-add-svg-filter",
|
|
BG_ADD_DYNAMIC_THEME: "bg-add-dynamic-theme",
|
|
BG_EXPORT_CSS: "bg-export-css",
|
|
BG_UNSUPPORTED_SENDER: "bg-unsupported-sender",
|
|
BG_CLEAN_UP: "bg-clean-up",
|
|
BG_RELOAD: "bg-reload",
|
|
BG_FETCH_RESPONSE: "bg-fetch-response",
|
|
BG_UI_UPDATE: "bg-ui-update",
|
|
BG_CSS_UPDATE: "bg-css-update",
|
|
CS_COLOR_SCHEME_CHANGE: "cs-color-scheme-change",
|
|
CS_FRAME_CONNECT: "cs-frame-connect",
|
|
CS_FRAME_FORGET: "cs-frame-forget",
|
|
CS_FRAME_FREEZE: "cs-frame-freeze",
|
|
CS_FRAME_RESUME: "cs-frame-resume",
|
|
CS_EXPORT_CSS_RESPONSE: "cs-export-css-response",
|
|
CS_FETCH: "cs-fetch"
|
|
};
|
|
|
|
class Messenger {
|
|
constructor(adapter) {
|
|
this.adapter = adapter;
|
|
this.changeListenerCount = 0;
|
|
const allowedSenderURL = [
|
|
chrome.runtime.getURL("/ui/popup/index.html"),
|
|
chrome.runtime.getURL("/ui/devtools/index.html"),
|
|
chrome.runtime.getURL("/ui/stylesheet-editor/index.html")
|
|
];
|
|
chrome.runtime.onMessage.addListener(
|
|
(message, sender, sendResponse) => {
|
|
if (allowedSenderURL.includes(sender.url)) {
|
|
this.onUIMessage(message, sendResponse);
|
|
this.adapter.onPopupOpen();
|
|
return [
|
|
MessageType.UI_GET_DATA,
|
|
MessageType.UI_GET_ACTIVE_TAB_INFO
|
|
].includes(message.type);
|
|
}
|
|
}
|
|
);
|
|
if (isFirefox) {
|
|
chrome.runtime.onConnect.addListener((port) => {
|
|
let promise;
|
|
switch (port.name) {
|
|
case MessageType.UI_GET_DATA:
|
|
promise = this.adapter.collect();
|
|
break;
|
|
case MessageType.UI_GET_ACTIVE_TAB_INFO:
|
|
promise = this.adapter.getActiveTabInfo();
|
|
break;
|
|
case MessageType.UI_APPLY_DEV_DYNAMIC_THEME_FIXES:
|
|
case MessageType.UI_APPLY_DEV_INVERSION_FIXES:
|
|
case MessageType.UI_APPLY_DEV_STATIC_THEMES:
|
|
promise = new Promise((resolve, reject) => {
|
|
port.onMessage.addListener((message) => {
|
|
const {data} = message;
|
|
let error;
|
|
switch (port.name) {
|
|
case MessageType.UI_APPLY_DEV_DYNAMIC_THEME_FIXES:
|
|
error =
|
|
this.adapter.applyDevDynamicThemeFixes(
|
|
data
|
|
);
|
|
break;
|
|
case MessageType.UI_APPLY_DEV_INVERSION_FIXES:
|
|
error =
|
|
this.adapter.applyDevInversionFixes(
|
|
data
|
|
);
|
|
break;
|
|
case MessageType.UI_APPLY_DEV_STATIC_THEMES:
|
|
error =
|
|
this.adapter.applyDevStaticThemes(
|
|
data
|
|
);
|
|
break;
|
|
default:
|
|
throw new Error(
|
|
`Unknown port name: ${port.name}`
|
|
);
|
|
}
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
promise
|
|
.then((data) => port.postMessage({data}))
|
|
.catch((error) => port.postMessage({error}));
|
|
});
|
|
}
|
|
}
|
|
onUIMessage({type, data}, sendResponse) {
|
|
switch (type) {
|
|
case MessageType.UI_GET_DATA: {
|
|
this.adapter.collect().then((data) => sendResponse({data}));
|
|
break;
|
|
}
|
|
case MessageType.UI_GET_ACTIVE_TAB_INFO: {
|
|
this.adapter
|
|
.getActiveTabInfo()
|
|
.then((data) => sendResponse({data}));
|
|
break;
|
|
}
|
|
case MessageType.UI_SUBSCRIBE_TO_CHANGES: {
|
|
this.changeListenerCount++;
|
|
break;
|
|
}
|
|
case MessageType.UI_UNSUBSCRIBE_FROM_CHANGES: {
|
|
this.changeListenerCount--;
|
|
break;
|
|
}
|
|
case MessageType.UI_CHANGE_SETTINGS: {
|
|
this.adapter.changeSettings(data);
|
|
break;
|
|
}
|
|
case MessageType.UI_SET_THEME: {
|
|
this.adapter.setTheme(data);
|
|
break;
|
|
}
|
|
case MessageType.UI_SET_SHORTCUT: {
|
|
this.adapter.setShortcut(data);
|
|
break;
|
|
}
|
|
case MessageType.UI_TOGGLE_URL: {
|
|
this.adapter.toggleURL(data);
|
|
break;
|
|
}
|
|
case MessageType.UI_MARK_NEWS_AS_READ: {
|
|
this.adapter.markNewsAsRead(data);
|
|
break;
|
|
}
|
|
case MessageType.UI_LOAD_CONFIG: {
|
|
this.adapter.loadConfig(data);
|
|
break;
|
|
}
|
|
case MessageType.UI_APPLY_DEV_DYNAMIC_THEME_FIXES: {
|
|
const error = this.adapter.applyDevDynamicThemeFixes(data);
|
|
sendResponse({error: error ? error.message : null});
|
|
break;
|
|
}
|
|
case MessageType.UI_RESET_DEV_DYNAMIC_THEME_FIXES: {
|
|
this.adapter.resetDevDynamicThemeFixes();
|
|
break;
|
|
}
|
|
case MessageType.UI_APPLY_DEV_INVERSION_FIXES: {
|
|
const error = this.adapter.applyDevInversionFixes(data);
|
|
sendResponse({error: error ? error.message : null});
|
|
break;
|
|
}
|
|
case MessageType.UI_RESET_DEV_INVERSION_FIXES: {
|
|
this.adapter.resetDevInversionFixes();
|
|
break;
|
|
}
|
|
case MessageType.UI_APPLY_DEV_STATIC_THEMES: {
|
|
const error = this.adapter.applyDevStaticThemes(data);
|
|
sendResponse({error: error ? error.message : null});
|
|
break;
|
|
}
|
|
case MessageType.UI_RESET_DEV_STATIC_THEMES: {
|
|
this.adapter.resetDevStaticThemes();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
reportChanges(data) {
|
|
if (this.changeListenerCount > 0) {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.BG_CHANGES,
|
|
data
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function getUILanguage() {
|
|
let code;
|
|
if (
|
|
"i18n" in chrome &&
|
|
"getUILanguage" in chrome.i18n &&
|
|
typeof chrome.i18n.getUILanguage === "function"
|
|
) {
|
|
code = chrome.i18n.getUILanguage();
|
|
} else {
|
|
code = navigator.language.split("-")[0];
|
|
}
|
|
if (code.endsWith("-mac")) {
|
|
return code.substring(0, code.length - 4);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
const BLOG_URL = "https://darkreader.org/blog/";
|
|
const UNINSTALL_URL = "https://darkreader.org/goodluck/";
|
|
const helpLocales = [
|
|
"be",
|
|
"cs",
|
|
"de",
|
|
"en",
|
|
"es",
|
|
"fr",
|
|
"it",
|
|
"nl",
|
|
"pt",
|
|
"ru",
|
|
"tr",
|
|
"zh-CN",
|
|
"zh-TW"
|
|
];
|
|
function getHelpURL() {
|
|
const locale = getUILanguage();
|
|
const matchLocale =
|
|
helpLocales.find((hl) => hl === locale) ||
|
|
helpLocales.find((hl) => locale.startsWith(hl)) ||
|
|
"en";
|
|
return `https://darkreader.org/help/${matchLocale}/`;
|
|
}
|
|
function getBlogPostURL(postId) {
|
|
return `${BLOG_URL}${postId}/`;
|
|
}
|
|
|
|
class Mutex {
|
|
constructor() {
|
|
this.awaitingResolves = [];
|
|
this.locked = false;
|
|
}
|
|
isLocked() {
|
|
return this.locked;
|
|
}
|
|
async lock() {
|
|
if (!this.locked) {
|
|
this.locked = true;
|
|
return;
|
|
}
|
|
return new Promise((resolve) => {
|
|
this.awaitingResolves.push(resolve);
|
|
});
|
|
}
|
|
unlock() {
|
|
if (!this.locked) {
|
|
return;
|
|
}
|
|
this.locked = false;
|
|
setTimeout(() => this.executeNextOperation());
|
|
}
|
|
executeNextOperation() {
|
|
if (this.awaitingResolves.length > 0) {
|
|
const resolve = this.awaitingResolves.shift();
|
|
resolve();
|
|
}
|
|
}
|
|
}
|
|
|
|
function canInjectScript(url) {
|
|
if (isFirefox) {
|
|
return (
|
|
url &&
|
|
!url.startsWith("about:") &&
|
|
!url.startsWith("moz") &&
|
|
!url.startsWith("view-source:") &&
|
|
!url.startsWith("https://addons.mozilla.org/") &&
|
|
!isPDF(url)
|
|
);
|
|
}
|
|
if (isEdge) {
|
|
return (
|
|
url &&
|
|
!url.startsWith("chrome") &&
|
|
!url.startsWith("data") &&
|
|
!url.startsWith("devtools") &&
|
|
!url.startsWith("edge") &&
|
|
!url.startsWith("https://chrome.google.com/webstore") &&
|
|
!url.startsWith("https://microsoftedge.microsoft.com/addons") &&
|
|
!url.startsWith("view-source")
|
|
);
|
|
}
|
|
return (
|
|
url &&
|
|
!url.startsWith("chrome") &&
|
|
!url.startsWith("https://chrome.google.com/webstore") &&
|
|
!url.startsWith("data") &&
|
|
!url.startsWith("devtools") &&
|
|
!url.startsWith("view-source")
|
|
);
|
|
}
|
|
const mutexStorageWriting = new Mutex();
|
|
async function readSyncStorage(defaults) {
|
|
return new Promise((resolve) => {
|
|
chrome.storage.sync.get(null, (sync) => {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(chrome.runtime.lastError.message);
|
|
resolve(defaults);
|
|
return;
|
|
}
|
|
for (const key in sync) {
|
|
if (!sync[key]) {
|
|
continue;
|
|
}
|
|
const metaKeysCount = sync[key].__meta_split_count;
|
|
if (!metaKeysCount) {
|
|
continue;
|
|
}
|
|
let string = "";
|
|
for (let i = 0; i < metaKeysCount; i++) {
|
|
string += sync[`${key}_${i.toString(36)}`];
|
|
delete sync[`${key}_${i.toString(36)}`];
|
|
}
|
|
try {
|
|
sync[key] = JSON.parse(string);
|
|
} catch (error) {
|
|
console.error(
|
|
"Could not parse record from sync storage",
|
|
string
|
|
);
|
|
}
|
|
}
|
|
sync = {
|
|
...defaults,
|
|
...sync
|
|
};
|
|
resolve(sync);
|
|
});
|
|
});
|
|
}
|
|
async function readLocalStorage(defaults) {
|
|
return new Promise((resolve) => {
|
|
chrome.storage.local.get(defaults, (local) => {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(chrome.runtime.lastError.message);
|
|
resolve(defaults);
|
|
return;
|
|
}
|
|
resolve(local);
|
|
});
|
|
});
|
|
}
|
|
function prepareSyncStorage(values) {
|
|
for (const key in values) {
|
|
const value = values[key];
|
|
const string = JSON.stringify(value);
|
|
const totalLength = string.length + key.length;
|
|
if (totalLength > chrome.storage.sync.QUOTA_BYTES_PER_ITEM) {
|
|
const maxLength =
|
|
chrome.storage.sync.QUOTA_BYTES_PER_ITEM -
|
|
key.length -
|
|
1 -
|
|
2;
|
|
const minimalKeysNeeded = Math.ceil(string.length / maxLength);
|
|
for (let i = 0; i < minimalKeysNeeded; i++) {
|
|
values[`${key}_${i.toString(36)}`] = string.substring(
|
|
i * maxLength,
|
|
(i + 1) * maxLength
|
|
);
|
|
}
|
|
values[key] = {
|
|
__meta_split_count: minimalKeysNeeded
|
|
};
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
async function writeSyncStorage(values) {
|
|
return new Promise(async (resolve, reject) => {
|
|
const packaged = prepareSyncStorage(values);
|
|
await mutexStorageWriting.lock();
|
|
chrome.storage.sync.set(packaged, () => {
|
|
if (chrome.runtime.lastError) {
|
|
reject(chrome.runtime.lastError);
|
|
mutexStorageWriting.unlock();
|
|
return;
|
|
}
|
|
resolve();
|
|
setTimeout(() => mutexStorageWriting.unlock(), 500);
|
|
});
|
|
});
|
|
}
|
|
async function writeLocalStorage(values) {
|
|
return new Promise(async (resolve) => {
|
|
await mutexStorageWriting.lock();
|
|
chrome.storage.local.set(values, () => {
|
|
resolve();
|
|
setTimeout(() => mutexStorageWriting.unlock(), 500);
|
|
});
|
|
});
|
|
}
|
|
const subscribeToOuterSettingsChange = (callback) => {
|
|
chrome.storage.onChanged.addListener((_, storageArea) => {
|
|
if (storageArea === "sync" && !mutexStorageWriting.isLocked()) {
|
|
callback();
|
|
}
|
|
});
|
|
};
|
|
async function getCommands() {
|
|
return new Promise((resolve) => {
|
|
if (!chrome.commands) {
|
|
resolve([]);
|
|
return;
|
|
}
|
|
chrome.commands.getAll((commands) => {
|
|
if (commands) {
|
|
resolve(commands);
|
|
} else {
|
|
resolve([]);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
function setShortcut(command, shortcut) {
|
|
if (
|
|
typeof browser !== "undefined" &&
|
|
browser.commands &&
|
|
browser.commands.update
|
|
) {
|
|
browser.commands.update({name: command, shortcut});
|
|
}
|
|
}
|
|
|
|
class PromiseBarrier {
|
|
constructor() {
|
|
this.resolves = [];
|
|
this.rejects = [];
|
|
this.wasResolved = false;
|
|
this.wasRejected = false;
|
|
}
|
|
async entry() {
|
|
if (this.wasResolved) {
|
|
return Promise.resolve(this.resolution);
|
|
}
|
|
if (this.wasRejected) {
|
|
return Promise.reject(this.reason);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
this.resolves.push(resolve);
|
|
this.rejects.push(reject);
|
|
});
|
|
}
|
|
async resolve(value) {
|
|
if (this.wasRejected || this.wasResolved) {
|
|
return;
|
|
}
|
|
this.wasResolved = true;
|
|
this.resolution = value;
|
|
this.resolves.forEach((resolve) => resolve(value));
|
|
this.resolves = null;
|
|
this.rejects = null;
|
|
return new Promise((resolve) => setTimeout(() => resolve()));
|
|
}
|
|
async reject(reason) {
|
|
if (this.wasRejected || this.wasResolved) {
|
|
return;
|
|
}
|
|
this.wasRejected = true;
|
|
this.reason = reason;
|
|
this.rejects.forEach((reject) => reject(reason));
|
|
this.resolves = null;
|
|
this.rejects = null;
|
|
return new Promise((resolve) => setTimeout(() => resolve()));
|
|
}
|
|
isPending() {
|
|
return !this.wasResolved && !this.wasRejected;
|
|
}
|
|
isFulfilled() {
|
|
return this.wasResolved;
|
|
}
|
|
isRejected() {
|
|
return this.wasRejected;
|
|
}
|
|
}
|
|
|
|
function isNonPersistent() {
|
|
if (!isChromium) {
|
|
return false;
|
|
}
|
|
const background = chrome.runtime.getManifest().background;
|
|
if ("persistent" in background) {
|
|
return background.persistent === false;
|
|
}
|
|
if ("service_worker" in background) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var StateManagerState;
|
|
(function (StateManagerState) {
|
|
StateManagerState[(StateManagerState["INITIAL"] = 0)] = "INITIAL";
|
|
StateManagerState[(StateManagerState["DISABLED"] = 1)] = "DISABLED";
|
|
StateManagerState[(StateManagerState["LOADING"] = 2)] = "LOADING";
|
|
StateManagerState[(StateManagerState["READY"] = 3)] = "READY";
|
|
StateManagerState[(StateManagerState["SAVING"] = 4)] = "SAVING";
|
|
StateManagerState[(StateManagerState["SAVING_OVERRIDE"] = 5)] =
|
|
"SAVING_OVERRIDE";
|
|
})(StateManagerState || (StateManagerState = {}));
|
|
class StateManager {
|
|
constructor(localStorageKey, parent, defaults) {
|
|
this.meta = StateManagerState.INITIAL;
|
|
this.loadStateBarrier = null;
|
|
if (!isNonPersistent()) {
|
|
this.meta = StateManagerState.DISABLED;
|
|
return;
|
|
}
|
|
this.localStorageKey = localStorageKey;
|
|
this.parent = parent;
|
|
this.defaults = defaults;
|
|
}
|
|
collectState() {
|
|
const state = {};
|
|
for (const key of Object.keys(this.defaults)) {
|
|
state[key] = this.parent[key] || this.defaults[key];
|
|
}
|
|
return state;
|
|
}
|
|
async saveState() {
|
|
switch (this.meta) {
|
|
case StateManagerState.DISABLED:
|
|
return;
|
|
case StateManagerState.LOADING:
|
|
case StateManagerState.INITIAL:
|
|
if (this.loadStateBarrier) {
|
|
await this.loadStateBarrier.entry();
|
|
}
|
|
this.meta = StateManagerState.SAVING;
|
|
break;
|
|
case StateManagerState.READY:
|
|
this.meta = StateManagerState.SAVING;
|
|
break;
|
|
case StateManagerState.SAVING:
|
|
this.meta = StateManagerState.SAVING_OVERRIDE;
|
|
return;
|
|
case StateManagerState.SAVING_OVERRIDE:
|
|
return;
|
|
}
|
|
chrome.storage.local.set(
|
|
{[this.localStorageKey]: this.collectState()},
|
|
() => {
|
|
switch (this.meta) {
|
|
case StateManagerState.INITIAL:
|
|
case StateManagerState.DISABLED:
|
|
case StateManagerState.LOADING:
|
|
case StateManagerState.READY:
|
|
case StateManagerState.SAVING:
|
|
this.meta = StateManagerState.READY;
|
|
break;
|
|
case StateManagerState.SAVING_OVERRIDE:
|
|
this.meta = StateManagerState.READY;
|
|
this.saveState();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
async loadState() {
|
|
switch (this.meta) {
|
|
case StateManagerState.INITIAL:
|
|
this.meta = StateManagerState.LOADING;
|
|
this.loadStateBarrier = new PromiseBarrier();
|
|
return new Promise((resolve) => {
|
|
chrome.storage.local.get(
|
|
this.localStorageKey,
|
|
(data) => {
|
|
this.meta = StateManagerState.READY;
|
|
if (data[this.localStorageKey]) {
|
|
Object.assign(
|
|
this.parent,
|
|
data[this.localStorageKey]
|
|
);
|
|
} else {
|
|
Object.assign(this.parent, this.defaults);
|
|
}
|
|
this.loadStateBarrier.resolve();
|
|
this.loadStateBarrier = null;
|
|
resolve();
|
|
}
|
|
);
|
|
});
|
|
case StateManagerState.DISABLED:
|
|
case StateManagerState.READY:
|
|
case StateManagerState.SAVING:
|
|
case StateManagerState.SAVING_OVERRIDE:
|
|
return;
|
|
case StateManagerState.LOADING:
|
|
return this.loadStateBarrier.entry();
|
|
}
|
|
}
|
|
}
|
|
|
|
class Newsmaker {
|
|
constructor(onUpdate) {
|
|
this.alarmListener = (alarm) => {
|
|
if (alarm.name === Newsmaker.ALARM_NAME) {
|
|
this.updateNews();
|
|
}
|
|
};
|
|
this.stateManager = new StateManager(
|
|
Newsmaker.LOCAL_STORAGE_KEY,
|
|
this,
|
|
{latest: [], latestTimestamp: null}
|
|
);
|
|
this.latest = [];
|
|
this.latestTimestamp = null;
|
|
this.onUpdate = onUpdate;
|
|
}
|
|
async getLatest() {
|
|
await this.stateManager.loadState();
|
|
return this.latest;
|
|
}
|
|
subscribe() {
|
|
if (
|
|
this.latestTimestamp === null ||
|
|
this.latestTimestamp + Newsmaker.UPDATE_INTERVAL < Date.now()
|
|
) {
|
|
this.updateNews();
|
|
}
|
|
chrome.alarms.onAlarm.addListener((alarm) =>
|
|
this.alarmListener(alarm)
|
|
);
|
|
chrome.alarms.create(Newsmaker.ALARM_NAME, {
|
|
periodInMinutes: Newsmaker.UPDATE_INTERVAL
|
|
});
|
|
}
|
|
unSubscribe() {
|
|
chrome.alarms.onAlarm.removeListener(this.alarmListener);
|
|
chrome.alarms.clear(Newsmaker.ALARM_NAME);
|
|
}
|
|
async updateNews() {
|
|
const news = await this.getNews();
|
|
if (Array.isArray(news)) {
|
|
this.latest = news;
|
|
this.latestTimestamp = Date.now();
|
|
this.onUpdate(this.latest);
|
|
await this.stateManager.saveState();
|
|
}
|
|
}
|
|
async getReadNews() {
|
|
const sync = await readSyncStorage({readNews: []});
|
|
const local = await readLocalStorage({readNews: []});
|
|
return Array.from(
|
|
new Set([
|
|
...(sync ? sync.readNews : []),
|
|
...(local ? local.readNews : [])
|
|
])
|
|
);
|
|
}
|
|
async getNews() {
|
|
try {
|
|
const response = await fetch(
|
|
`https://darkreader.github.io/blog/posts.json`,
|
|
{cache: "no-cache"}
|
|
);
|
|
const $news = await response.json();
|
|
const readNews = await this.getReadNews();
|
|
const news = $news.map(({id, date, headline, important}) => {
|
|
const url = getBlogPostURL(id);
|
|
const read = this.isRead(id, readNews);
|
|
return {id, date, headline, url, important, read};
|
|
});
|
|
for (let i = 0; i < news.length; i++) {
|
|
const date = new Date(news[i].date);
|
|
if (isNaN(date.getTime())) {
|
|
throw new Error(`Unable to parse date ${date}`);
|
|
}
|
|
}
|
|
return news;
|
|
} catch (err) {
|
|
console.error(err);
|
|
return null;
|
|
}
|
|
}
|
|
async markAsRead(...ids) {
|
|
const readNews = await this.getReadNews();
|
|
const results = readNews.slice();
|
|
let changed = false;
|
|
ids.forEach((id) => {
|
|
if (readNews.indexOf(id) < 0) {
|
|
results.push(id);
|
|
changed = true;
|
|
}
|
|
});
|
|
if (changed) {
|
|
this.latest = this.latest.map(
|
|
({id, date, url, headline, important}) => {
|
|
const read = this.isRead(id, results);
|
|
return {id, date, url, headline, important, read};
|
|
}
|
|
);
|
|
this.onUpdate(this.latest);
|
|
const obj = {readNews: results};
|
|
await writeLocalStorage(obj);
|
|
await writeSyncStorage(obj);
|
|
await this.stateManager.saveState();
|
|
}
|
|
}
|
|
isRead(id, readNews) {
|
|
return readNews.includes(id);
|
|
}
|
|
}
|
|
Newsmaker.UPDATE_INTERVAL = getDurationInMinutes({hours: 4});
|
|
Newsmaker.ALARM_NAME = "newsmaker";
|
|
Newsmaker.LOCAL_STORAGE_KEY = "Newsmaker-state";
|
|
|
|
async function queryTabs(query) {
|
|
return new Promise((resolve) => {
|
|
chrome.tabs.query(query, (tabs) => resolve(tabs));
|
|
});
|
|
}
|
|
var DocumentState;
|
|
(function (DocumentState) {
|
|
DocumentState[(DocumentState["ACTIVE"] = 0)] = "ACTIVE";
|
|
DocumentState[(DocumentState["PASSIVE"] = 1)] = "PASSIVE";
|
|
DocumentState[(DocumentState["HIDDEN"] = 2)] = "HIDDEN";
|
|
DocumentState[(DocumentState["FROZEN"] = 3)] = "FROZEN";
|
|
DocumentState[(DocumentState["TERMINATED"] = 4)] = "TERMINATED";
|
|
DocumentState[(DocumentState["DISCARDED"] = 5)] = "DISCARDED";
|
|
})(DocumentState || (DocumentState = {}));
|
|
class TabManager {
|
|
constructor({
|
|
getConnectionMessage,
|
|
onColorSchemeChange,
|
|
getTabMessage
|
|
}) {
|
|
this.fileLoader = null;
|
|
this.timestamp = null;
|
|
this.stateManager = new StateManager(
|
|
TabManager.LOCAL_STORAGE_KEY,
|
|
this,
|
|
{tabs: {}, timestamp: 0}
|
|
);
|
|
this.tabs = {};
|
|
this.getTabMessage = getTabMessage;
|
|
chrome.runtime.onMessage.addListener(
|
|
async (message, sender, sendResponse) => {
|
|
function addFrame(
|
|
tabs,
|
|
tabId,
|
|
frameId,
|
|
senderURL,
|
|
timestamp
|
|
) {
|
|
let frames;
|
|
if (tabs[tabId]) {
|
|
frames = tabs[tabId];
|
|
} else {
|
|
frames = {};
|
|
tabs[tabId] = frames;
|
|
}
|
|
frames[frameId] = {
|
|
url: senderURL,
|
|
state: DocumentState.ACTIVE,
|
|
timestamp
|
|
};
|
|
}
|
|
switch (message.type) {
|
|
case MessageType.CS_FRAME_CONNECT: {
|
|
await this.stateManager.loadState();
|
|
const reply = (options) => {
|
|
const message = getConnectionMessage(options);
|
|
if (message instanceof Promise) {
|
|
message.then(
|
|
(asyncMessage) =>
|
|
asyncMessage &&
|
|
chrome.tabs.sendMessage(
|
|
sender.tab.id,
|
|
asyncMessage,
|
|
{frameId: sender.frameId}
|
|
)
|
|
);
|
|
} else if (message) {
|
|
chrome.tabs.sendMessage(
|
|
sender.tab.id,
|
|
message,
|
|
{frameId: sender.frameId}
|
|
);
|
|
}
|
|
};
|
|
const isPanel =
|
|
typeof sender === "undefined" ||
|
|
typeof sender.tab === "undefined";
|
|
if (isPanel) {
|
|
if (isFirefox) {
|
|
if (
|
|
sender &&
|
|
sender.tab &&
|
|
typeof sender.tab.id === "number"
|
|
) {
|
|
chrome.tabs.sendMessage(
|
|
sender.tab.id,
|
|
{
|
|
type: MessageType.BG_UNSUPPORTED_SENDER
|
|
},
|
|
{
|
|
frameId:
|
|
sender &&
|
|
typeof sender.frameId ===
|
|
"number"
|
|
? sender.frameId
|
|
: undefined
|
|
}
|
|
);
|
|
}
|
|
} else {
|
|
sendResponse("unsupportedSender");
|
|
}
|
|
return;
|
|
}
|
|
const tabId = sender.tab.id;
|
|
const {frameId} = sender;
|
|
const senderURL = sender.url;
|
|
const tabURL = sender.tab.url;
|
|
addFrame(
|
|
this.tabs,
|
|
tabId,
|
|
frameId,
|
|
senderURL,
|
|
this.timestamp
|
|
);
|
|
reply({
|
|
url: tabURL,
|
|
frameURL: frameId === 0 ? null : senderURL
|
|
});
|
|
this.stateManager.saveState();
|
|
sendResponse({type: "¯\\_(ツ)_/¯"});
|
|
break;
|
|
}
|
|
case MessageType.CS_FRAME_FORGET: {
|
|
await this.stateManager.loadState();
|
|
if (!sender.tab) {
|
|
break;
|
|
}
|
|
const tabId = sender.tab.id;
|
|
const frameId = sender.frameId;
|
|
if (frameId === 0) {
|
|
delete this.tabs[tabId];
|
|
}
|
|
if (this.tabs[tabId] && this.tabs[tabId][frameId]) {
|
|
delete this.tabs[tabId][frameId];
|
|
}
|
|
this.stateManager.saveState();
|
|
break;
|
|
}
|
|
case MessageType.CS_FRAME_FREEZE:
|
|
await this.stateManager.loadState();
|
|
const info =
|
|
this.tabs[sender.tab.id][sender.frameId];
|
|
info.state = DocumentState.FROZEN;
|
|
info.url = null;
|
|
this.stateManager.saveState();
|
|
break;
|
|
case MessageType.CS_FRAME_RESUME: {
|
|
await this.stateManager.loadState();
|
|
const tabId = sender.tab.id;
|
|
const frameId = sender.frameId;
|
|
const frameURL = sender.url;
|
|
if (
|
|
this.tabs[tabId][frameId].timestamp <
|
|
this.timestamp
|
|
) {
|
|
const message = this.getTabMessage(
|
|
this.getTabURL(sender.tab),
|
|
frameURL
|
|
);
|
|
chrome.tabs.sendMessage(tabId, message, {
|
|
frameId
|
|
});
|
|
}
|
|
this.tabs[sender.tab.id][sender.frameId] = {
|
|
url: sender.url,
|
|
state: DocumentState.ACTIVE,
|
|
timestamp: this.timestamp
|
|
};
|
|
this.stateManager.saveState();
|
|
break;
|
|
}
|
|
case MessageType.CS_FETCH: {
|
|
const id = message.id;
|
|
const sendResponse = (response) =>
|
|
chrome.tabs.sendMessage(sender.tab.id, {
|
|
type: MessageType.BG_FETCH_RESPONSE,
|
|
id,
|
|
...response
|
|
});
|
|
if (isThunderbird) {
|
|
if (message.data.url.startsWith("chrome://")) {
|
|
sendResponse({data: null});
|
|
return;
|
|
}
|
|
}
|
|
try {
|
|
const {url, responseType, mimeType, origin} =
|
|
message.data;
|
|
if (!this.fileLoader) {
|
|
this.fileLoader = createFileLoader();
|
|
}
|
|
const response = await this.fileLoader.get({
|
|
url,
|
|
responseType,
|
|
mimeType,
|
|
origin
|
|
});
|
|
sendResponse({data: response});
|
|
} catch (err) {
|
|
sendResponse({
|
|
error:
|
|
err && err.message ? err.message : err
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case MessageType.CS_COLOR_SCHEME_CHANGE: {
|
|
onColorSchemeChange(message.data);
|
|
break;
|
|
}
|
|
case MessageType.UI_SAVE_FILE: {
|
|
const {content, name} = message.data;
|
|
const a = document.createElement("a");
|
|
a.href = URL.createObjectURL(new Blob([content]));
|
|
a.download = name;
|
|
a.click();
|
|
break;
|
|
}
|
|
case MessageType.UI_REQUEST_EXPORT_CSS: {
|
|
const activeTab = await this.getActiveTab();
|
|
chrome.tabs.sendMessage(
|
|
activeTab.id,
|
|
{type: MessageType.BG_EXPORT_CSS},
|
|
{frameId: 0}
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
getTabURL(tab) {
|
|
return tab.url || "about:blank";
|
|
}
|
|
async updateContentScript(options) {
|
|
(await queryTabs({}))
|
|
.filter(
|
|
(tab) =>
|
|
options.runOnProtectedPages || canInjectScript(tab.url)
|
|
)
|
|
.filter((tab) => !Boolean(this.tabs[tab.id]))
|
|
.forEach((tab) => {
|
|
if (!tab.discarded) {
|
|
if (isMV3) {
|
|
chrome.scripting.executeScript({
|
|
target: {
|
|
tabId: tab.id,
|
|
allFrames: true
|
|
},
|
|
files: ["/inject/index.js"]
|
|
});
|
|
} else {
|
|
chrome.tabs.executeScript(tab.id, {
|
|
runAt: "document_start",
|
|
file: "/inject/index.js",
|
|
allFrames: true,
|
|
matchAboutBlank: true
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
async registerMailDisplayScript() {
|
|
await chrome.messageDisplayScripts.register({
|
|
js: [{file: "/inject/fallback.js"}, {file: "/inject/index.js"}]
|
|
});
|
|
}
|
|
async sendMessage() {
|
|
this.timestamp++;
|
|
(await queryTabs({}))
|
|
.filter((tab) => Boolean(this.tabs[tab.id]))
|
|
.forEach((tab) => {
|
|
const frames = this.tabs[tab.id];
|
|
Object.entries(frames)
|
|
.filter(
|
|
([, {state}]) =>
|
|
state === DocumentState.ACTIVE ||
|
|
state === DocumentState.PASSIVE
|
|
)
|
|
.forEach(([, {url}], frameId) => {
|
|
const message = this.getTabMessage(
|
|
this.getTabURL(tab),
|
|
frameId === 0 ? null : url
|
|
);
|
|
if (tab.active && frameId === 0) {
|
|
chrome.tabs.sendMessage(tab.id, message, {
|
|
frameId
|
|
});
|
|
} else {
|
|
setTimeout(() =>
|
|
chrome.tabs.sendMessage(tab.id, message, {
|
|
frameId
|
|
})
|
|
);
|
|
}
|
|
if (this.tabs[tab.id][frameId]) {
|
|
this.tabs[tab.id][frameId].timestamp =
|
|
this.timestamp;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
async canAccessActiveTab() {
|
|
const tab = await this.getActiveTab();
|
|
return Boolean(this.tabs[tab.id]);
|
|
}
|
|
async getActiveTabURL() {
|
|
return this.getTabURL(await this.getActiveTab());
|
|
}
|
|
async getActiveTab() {
|
|
let tab = (
|
|
await queryTabs({
|
|
active: true,
|
|
lastFocusedWindow: true
|
|
})
|
|
)[0];
|
|
const isExtensionPage = (url) =>
|
|
url.startsWith("chrome-extension:") ||
|
|
url.startsWith("moz-extension:");
|
|
if (!tab || isExtensionPage(tab.url)) {
|
|
const tabs = await queryTabs({active: true});
|
|
tab = tabs.find((t) => !isExtensionPage(t.url)) || tab;
|
|
}
|
|
return tab;
|
|
}
|
|
}
|
|
TabManager.LOCAL_STORAGE_KEY = "TabManager-state";
|
|
|
|
function debounce(delay, fn) {
|
|
let timeoutId = null;
|
|
return (...args) => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
timeoutId = setTimeout(() => {
|
|
timeoutId = null;
|
|
fn(...args);
|
|
}, delay);
|
|
};
|
|
}
|
|
|
|
const SAVE_TIMEOUT = 1000;
|
|
class UserStorage {
|
|
constructor({onRemoteSettingsChange}) {
|
|
this.saveSettingsIntoStorage = debounce(SAVE_TIMEOUT, async () => {
|
|
if (this.saveStorageBarrier) {
|
|
await this.saveStorageBarrier.entry();
|
|
return;
|
|
}
|
|
this.saveStorageBarrier = new PromiseBarrier();
|
|
const settings = this.settings;
|
|
if (settings.syncSettings) {
|
|
try {
|
|
await writeSyncStorage(settings);
|
|
} catch (err) {
|
|
logWarn(
|
|
"Settings synchronization was disabled due to error:",
|
|
chrome.runtime.lastError
|
|
);
|
|
this.set({syncSettings: false});
|
|
await this.saveSyncSetting(false);
|
|
await writeLocalStorage(settings);
|
|
}
|
|
} else {
|
|
await writeLocalStorage(settings);
|
|
}
|
|
this.saveStorageBarrier.resolve();
|
|
this.saveStorageBarrier = null;
|
|
});
|
|
this.settings = null;
|
|
subscribeToOuterSettingsChange(async () => {
|
|
await this.loadSettings();
|
|
onRemoteSettingsChange();
|
|
});
|
|
}
|
|
async loadSettings() {
|
|
this.settings = await this.loadSettingsFromStorage();
|
|
}
|
|
fillDefaults(settings) {
|
|
settings.theme = {...DEFAULT_THEME, ...settings.theme};
|
|
settings.time = {...DEFAULT_SETTINGS.time, ...settings.time};
|
|
settings.presets.forEach((preset) => {
|
|
preset.theme = {...DEFAULT_THEME, ...preset.theme};
|
|
});
|
|
settings.customThemes.forEach((site) => {
|
|
site.theme = {...DEFAULT_THEME, ...site.theme};
|
|
});
|
|
}
|
|
async loadSettingsFromStorage() {
|
|
if (this.loadBarrier) {
|
|
return await this.loadBarrier.entry();
|
|
}
|
|
this.loadBarrier = new PromiseBarrier();
|
|
const local = await readLocalStorage(DEFAULT_SETTINGS);
|
|
if (local.syncSettings == null) {
|
|
local.syncSettings = DEFAULT_SETTINGS.syncSettings;
|
|
}
|
|
if (!local.syncSettings) {
|
|
this.fillDefaults(local);
|
|
this.loadBarrier.resolve(local);
|
|
return local;
|
|
}
|
|
const $sync = await readSyncStorage(DEFAULT_SETTINGS);
|
|
if (!$sync) {
|
|
local.syncSettings = false;
|
|
this.set({syncSettings: false});
|
|
this.saveSyncSetting(false);
|
|
this.loadBarrier.resolve(local);
|
|
return local;
|
|
}
|
|
const sync = await readSyncStorage(DEFAULT_SETTINGS);
|
|
this.fillDefaults(sync);
|
|
this.loadBarrier.resolve(sync);
|
|
return sync;
|
|
}
|
|
async saveSettings() {
|
|
await this.saveSettingsIntoStorage();
|
|
}
|
|
async saveSyncSetting(sync) {
|
|
const obj = {syncSettings: sync};
|
|
await writeLocalStorage(obj);
|
|
try {
|
|
await writeSyncStorage(obj);
|
|
} catch (err) {
|
|
logWarn(
|
|
"Settings synchronization was disabled due to error:",
|
|
chrome.runtime.lastError
|
|
);
|
|
this.set({syncSettings: false});
|
|
}
|
|
}
|
|
set($settings) {
|
|
if ($settings.siteList) {
|
|
if (!Array.isArray($settings.siteList)) {
|
|
const list = [];
|
|
for (const key in $settings.siteList) {
|
|
const index = Number(key);
|
|
if (!isNaN(index)) {
|
|
list[index] = $settings.siteList[key];
|
|
}
|
|
}
|
|
$settings.siteList = list;
|
|
}
|
|
const siteList = $settings.siteList.filter((pattern) => {
|
|
let isOK = false;
|
|
try {
|
|
isURLMatched("https://google.com/", pattern);
|
|
isURLMatched("[::1]:1337", pattern);
|
|
isOK = true;
|
|
} catch (err) {}
|
|
return isOK && pattern !== "/";
|
|
});
|
|
$settings = {...$settings, siteList};
|
|
}
|
|
this.settings = {...this.settings, ...$settings};
|
|
}
|
|
}
|
|
|
|
function hslToRGB({h, s, l, a = 1}) {
|
|
if (s === 0) {
|
|
const [r, b, g] = [l, l, l].map((x) => Math.round(x * 255));
|
|
return {r, g, b, a};
|
|
}
|
|
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
const m = l - c / 2;
|
|
const [r, g, b] = (
|
|
h < 60
|
|
? [c, x, 0]
|
|
: h < 120
|
|
? [x, c, 0]
|
|
: h < 180
|
|
? [0, c, x]
|
|
: h < 240
|
|
? [0, x, c]
|
|
: h < 300
|
|
? [x, 0, c]
|
|
: [c, 0, x]
|
|
).map((n) => Math.round((n + m) * 255));
|
|
return {r, g, b, a};
|
|
}
|
|
function rgbToHSL({r: r255, g: g255, b: b255, a = 1}) {
|
|
const r = r255 / 255;
|
|
const g = g255 / 255;
|
|
const b = b255 / 255;
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
const c = max - min;
|
|
const l = (max + min) / 2;
|
|
if (c === 0) {
|
|
return {h: 0, s: 0, l, a};
|
|
}
|
|
let h =
|
|
(max === r
|
|
? ((g - b) / c) % 6
|
|
: max === g
|
|
? (b - r) / c + 2
|
|
: (r - g) / c + 4) * 60;
|
|
if (h < 0) {
|
|
h += 360;
|
|
}
|
|
const s = c / (1 - Math.abs(2 * l - 1));
|
|
return {h, s, l, a};
|
|
}
|
|
function toFixed(n, digits = 0) {
|
|
const fixed = n.toFixed(digits);
|
|
if (digits === 0) {
|
|
return fixed;
|
|
}
|
|
const dot = fixed.indexOf(".");
|
|
if (dot >= 0) {
|
|
const zerosMatch = fixed.match(/0+$/);
|
|
if (zerosMatch) {
|
|
if (zerosMatch.index === dot + 1) {
|
|
return fixed.substring(0, dot);
|
|
}
|
|
return fixed.substring(0, zerosMatch.index);
|
|
}
|
|
}
|
|
return fixed;
|
|
}
|
|
function rgbToString(rgb) {
|
|
const {r, g, b, a} = rgb;
|
|
if (a != null && a < 1) {
|
|
return `rgba(${toFixed(r)}, ${toFixed(g)}, ${toFixed(b)}, ${toFixed(
|
|
a,
|
|
2
|
|
)})`;
|
|
}
|
|
return `rgb(${toFixed(r)}, ${toFixed(g)}, ${toFixed(b)})`;
|
|
}
|
|
function rgbToHexString({r, g, b, a}) {
|
|
return `#${(a != null && a < 1
|
|
? [r, g, b, Math.round(a * 255)]
|
|
: [r, g, b]
|
|
)
|
|
.map((x) => {
|
|
return `${x < 16 ? "0" : ""}${x.toString(16)}`;
|
|
})
|
|
.join("")}`;
|
|
}
|
|
const rgbMatch = /^rgba?\([^\(\)]+\)$/;
|
|
const hslMatch = /^hsla?\([^\(\)]+\)$/;
|
|
const hexMatch = /^#[0-9a-f]+$/i;
|
|
function parse($color) {
|
|
const c = $color.trim().toLowerCase();
|
|
if (c.match(rgbMatch)) {
|
|
return parseRGB(c);
|
|
}
|
|
if (c.match(hslMatch)) {
|
|
return parseHSL(c);
|
|
}
|
|
if (c.match(hexMatch)) {
|
|
return parseHex(c);
|
|
}
|
|
if (knownColors.has(c)) {
|
|
return getColorByName(c);
|
|
}
|
|
if (systemColors.has(c)) {
|
|
return getSystemColor(c);
|
|
}
|
|
if ($color === "transparent") {
|
|
return {r: 0, g: 0, b: 0, a: 0};
|
|
}
|
|
throw new Error(`Unable to parse ${$color}`);
|
|
}
|
|
function getNumbers($color) {
|
|
const numbers = [];
|
|
let prevPos = 0;
|
|
let isMining = false;
|
|
const startIndex = $color.indexOf("(");
|
|
$color = $color.substring(startIndex + 1, $color.length - 1);
|
|
for (let i = 0; i < $color.length; i++) {
|
|
const c = $color[i];
|
|
if ((c >= "0" && c <= "9") || c === "." || c === "+" || c === "-") {
|
|
isMining = true;
|
|
} else if (isMining && (c === " " || c === ",")) {
|
|
numbers.push($color.substring(prevPos, i));
|
|
isMining = false;
|
|
prevPos = i + 1;
|
|
} else if (!isMining) {
|
|
prevPos = i + 1;
|
|
}
|
|
}
|
|
if (isMining) {
|
|
numbers.push($color.substring(prevPos, $color.length));
|
|
}
|
|
return numbers;
|
|
}
|
|
function getNumbersFromString(str, range, units) {
|
|
const raw = getNumbers(str);
|
|
const unitsList = Object.entries(units);
|
|
const numbers = raw
|
|
.map((r) => r.trim())
|
|
.map((r, i) => {
|
|
let n;
|
|
const unit = unitsList.find(([u]) => r.endsWith(u));
|
|
if (unit) {
|
|
n =
|
|
(parseFloat(r.substring(0, r.length - unit[0].length)) /
|
|
unit[1]) *
|
|
range[i];
|
|
} else {
|
|
n = parseFloat(r);
|
|
}
|
|
if (range[i] > 1) {
|
|
return Math.round(n);
|
|
}
|
|
return n;
|
|
});
|
|
return numbers;
|
|
}
|
|
const rgbRange = [255, 255, 255, 1];
|
|
const rgbUnits = {"%": 100};
|
|
function parseRGB($rgb) {
|
|
const [r, g, b, a = 1] = getNumbersFromString($rgb, rgbRange, rgbUnits);
|
|
return {r, g, b, a};
|
|
}
|
|
const hslRange = [360, 1, 1, 1];
|
|
const hslUnits = {"%": 100, "deg": 360, "rad": 2 * Math.PI, "turn": 1};
|
|
function parseHSL($hsl) {
|
|
const [h, s, l, a = 1] = getNumbersFromString($hsl, hslRange, hslUnits);
|
|
return hslToRGB({h, s, l, a});
|
|
}
|
|
function parseHex($hex) {
|
|
const h = $hex.substring(1);
|
|
switch (h.length) {
|
|
case 3:
|
|
case 4: {
|
|
const [r, g, b] = [0, 1, 2].map((i) =>
|
|
parseInt(`${h[i]}${h[i]}`, 16)
|
|
);
|
|
const a =
|
|
h.length === 3 ? 1 : parseInt(`${h[3]}${h[3]}`, 16) / 255;
|
|
return {r, g, b, a};
|
|
}
|
|
case 6:
|
|
case 8: {
|
|
const [r, g, b] = [0, 2, 4].map((i) =>
|
|
parseInt(h.substring(i, i + 2), 16)
|
|
);
|
|
const a =
|
|
h.length === 6 ? 1 : parseInt(h.substring(6, 8), 16) / 255;
|
|
return {r, g, b, a};
|
|
}
|
|
}
|
|
throw new Error(`Unable to parse ${$hex}`);
|
|
}
|
|
function getColorByName($color) {
|
|
const n = knownColors.get($color);
|
|
return {
|
|
r: (n >> 16) & 255,
|
|
g: (n >> 8) & 255,
|
|
b: (n >> 0) & 255,
|
|
a: 1
|
|
};
|
|
}
|
|
function getSystemColor($color) {
|
|
const n = systemColors.get($color);
|
|
return {
|
|
r: (n >> 16) & 255,
|
|
g: (n >> 8) & 255,
|
|
b: (n >> 0) & 255,
|
|
a: 1
|
|
};
|
|
}
|
|
const knownColors = new Map(
|
|
Object.entries({
|
|
aliceblue: 0xf0f8ff,
|
|
antiquewhite: 0xfaebd7,
|
|
aqua: 0x00ffff,
|
|
aquamarine: 0x7fffd4,
|
|
azure: 0xf0ffff,
|
|
beige: 0xf5f5dc,
|
|
bisque: 0xffe4c4,
|
|
black: 0x000000,
|
|
blanchedalmond: 0xffebcd,
|
|
blue: 0x0000ff,
|
|
blueviolet: 0x8a2be2,
|
|
brown: 0xa52a2a,
|
|
burlywood: 0xdeb887,
|
|
cadetblue: 0x5f9ea0,
|
|
chartreuse: 0x7fff00,
|
|
chocolate: 0xd2691e,
|
|
coral: 0xff7f50,
|
|
cornflowerblue: 0x6495ed,
|
|
cornsilk: 0xfff8dc,
|
|
crimson: 0xdc143c,
|
|
cyan: 0x00ffff,
|
|
darkblue: 0x00008b,
|
|
darkcyan: 0x008b8b,
|
|
darkgoldenrod: 0xb8860b,
|
|
darkgray: 0xa9a9a9,
|
|
darkgrey: 0xa9a9a9,
|
|
darkgreen: 0x006400,
|
|
darkkhaki: 0xbdb76b,
|
|
darkmagenta: 0x8b008b,
|
|
darkolivegreen: 0x556b2f,
|
|
darkorange: 0xff8c00,
|
|
darkorchid: 0x9932cc,
|
|
darkred: 0x8b0000,
|
|
darksalmon: 0xe9967a,
|
|
darkseagreen: 0x8fbc8f,
|
|
darkslateblue: 0x483d8b,
|
|
darkslategray: 0x2f4f4f,
|
|
darkslategrey: 0x2f4f4f,
|
|
darkturquoise: 0x00ced1,
|
|
darkviolet: 0x9400d3,
|
|
deeppink: 0xff1493,
|
|
deepskyblue: 0x00bfff,
|
|
dimgray: 0x696969,
|
|
dimgrey: 0x696969,
|
|
dodgerblue: 0x1e90ff,
|
|
firebrick: 0xb22222,
|
|
floralwhite: 0xfffaf0,
|
|
forestgreen: 0x228b22,
|
|
fuchsia: 0xff00ff,
|
|
gainsboro: 0xdcdcdc,
|
|
ghostwhite: 0xf8f8ff,
|
|
gold: 0xffd700,
|
|
goldenrod: 0xdaa520,
|
|
gray: 0x808080,
|
|
grey: 0x808080,
|
|
green: 0x008000,
|
|
greenyellow: 0xadff2f,
|
|
honeydew: 0xf0fff0,
|
|
hotpink: 0xff69b4,
|
|
indianred: 0xcd5c5c,
|
|
indigo: 0x4b0082,
|
|
ivory: 0xfffff0,
|
|
khaki: 0xf0e68c,
|
|
lavender: 0xe6e6fa,
|
|
lavenderblush: 0xfff0f5,
|
|
lawngreen: 0x7cfc00,
|
|
lemonchiffon: 0xfffacd,
|
|
lightblue: 0xadd8e6,
|
|
lightcoral: 0xf08080,
|
|
lightcyan: 0xe0ffff,
|
|
lightgoldenrodyellow: 0xfafad2,
|
|
lightgray: 0xd3d3d3,
|
|
lightgrey: 0xd3d3d3,
|
|
lightgreen: 0x90ee90,
|
|
lightpink: 0xffb6c1,
|
|
lightsalmon: 0xffa07a,
|
|
lightseagreen: 0x20b2aa,
|
|
lightskyblue: 0x87cefa,
|
|
lightslategray: 0x778899,
|
|
lightslategrey: 0x778899,
|
|
lightsteelblue: 0xb0c4de,
|
|
lightyellow: 0xffffe0,
|
|
lime: 0x00ff00,
|
|
limegreen: 0x32cd32,
|
|
linen: 0xfaf0e6,
|
|
magenta: 0xff00ff,
|
|
maroon: 0x800000,
|
|
mediumaquamarine: 0x66cdaa,
|
|
mediumblue: 0x0000cd,
|
|
mediumorchid: 0xba55d3,
|
|
mediumpurple: 0x9370db,
|
|
mediumseagreen: 0x3cb371,
|
|
mediumslateblue: 0x7b68ee,
|
|
mediumspringgreen: 0x00fa9a,
|
|
mediumturquoise: 0x48d1cc,
|
|
mediumvioletred: 0xc71585,
|
|
midnightblue: 0x191970,
|
|
mintcream: 0xf5fffa,
|
|
mistyrose: 0xffe4e1,
|
|
moccasin: 0xffe4b5,
|
|
navajowhite: 0xffdead,
|
|
navy: 0x000080,
|
|
oldlace: 0xfdf5e6,
|
|
olive: 0x808000,
|
|
olivedrab: 0x6b8e23,
|
|
orange: 0xffa500,
|
|
orangered: 0xff4500,
|
|
orchid: 0xda70d6,
|
|
palegoldenrod: 0xeee8aa,
|
|
palegreen: 0x98fb98,
|
|
paleturquoise: 0xafeeee,
|
|
palevioletred: 0xdb7093,
|
|
papayawhip: 0xffefd5,
|
|
peachpuff: 0xffdab9,
|
|
peru: 0xcd853f,
|
|
pink: 0xffc0cb,
|
|
plum: 0xdda0dd,
|
|
powderblue: 0xb0e0e6,
|
|
purple: 0x800080,
|
|
rebeccapurple: 0x663399,
|
|
red: 0xff0000,
|
|
rosybrown: 0xbc8f8f,
|
|
royalblue: 0x4169e1,
|
|
saddlebrown: 0x8b4513,
|
|
salmon: 0xfa8072,
|
|
sandybrown: 0xf4a460,
|
|
seagreen: 0x2e8b57,
|
|
seashell: 0xfff5ee,
|
|
sienna: 0xa0522d,
|
|
silver: 0xc0c0c0,
|
|
skyblue: 0x87ceeb,
|
|
slateblue: 0x6a5acd,
|
|
slategray: 0x708090,
|
|
slategrey: 0x708090,
|
|
snow: 0xfffafa,
|
|
springgreen: 0x00ff7f,
|
|
steelblue: 0x4682b4,
|
|
tan: 0xd2b48c,
|
|
teal: 0x008080,
|
|
thistle: 0xd8bfd8,
|
|
tomato: 0xff6347,
|
|
turquoise: 0x40e0d0,
|
|
violet: 0xee82ee,
|
|
wheat: 0xf5deb3,
|
|
white: 0xffffff,
|
|
whitesmoke: 0xf5f5f5,
|
|
yellow: 0xffff00,
|
|
yellowgreen: 0x9acd32
|
|
})
|
|
);
|
|
const systemColors = new Map(
|
|
Object.entries({
|
|
"ActiveBorder": 0x3b99fc,
|
|
"ActiveCaption": 0x000000,
|
|
"AppWorkspace": 0xaaaaaa,
|
|
"Background": 0x6363ce,
|
|
"ButtonFace": 0xffffff,
|
|
"ButtonHighlight": 0xe9e9e9,
|
|
"ButtonShadow": 0x9fa09f,
|
|
"ButtonText": 0x000000,
|
|
"CaptionText": 0x000000,
|
|
"GrayText": 0x7f7f7f,
|
|
"Highlight": 0xb2d7ff,
|
|
"HighlightText": 0x000000,
|
|
"InactiveBorder": 0xffffff,
|
|
"InactiveCaption": 0xffffff,
|
|
"InactiveCaptionText": 0x000000,
|
|
"InfoBackground": 0xfbfcc5,
|
|
"InfoText": 0x000000,
|
|
"Menu": 0xf6f6f6,
|
|
"MenuText": 0xffffff,
|
|
"Scrollbar": 0xaaaaaa,
|
|
"ThreeDDarkShadow": 0x000000,
|
|
"ThreeDFace": 0xc0c0c0,
|
|
"ThreeDHighlight": 0xffffff,
|
|
"ThreeDLightShadow": 0xffffff,
|
|
"ThreeDShadow": 0x000000,
|
|
"Window": 0xececec,
|
|
"WindowFrame": 0xaaaaaa,
|
|
"WindowText": 0x000000,
|
|
"-webkit-focus-ring-color": 0xe59700
|
|
}).map(([key, value]) => [key.toLowerCase(), value])
|
|
);
|
|
|
|
function getBgPole(theme) {
|
|
const isDarkScheme = theme.mode === 1;
|
|
const prop = isDarkScheme
|
|
? "darkSchemeBackgroundColor"
|
|
: "lightSchemeBackgroundColor";
|
|
return theme[prop];
|
|
}
|
|
function getFgPole(theme) {
|
|
const isDarkScheme = theme.mode === 1;
|
|
const prop = isDarkScheme
|
|
? "darkSchemeTextColor"
|
|
: "lightSchemeTextColor";
|
|
return theme[prop];
|
|
}
|
|
const colorModificationCache = new Map();
|
|
const colorParseCache = new Map();
|
|
function parseToHSLWithCache(color) {
|
|
if (colorParseCache.has(color)) {
|
|
return colorParseCache.get(color);
|
|
}
|
|
const rgb = parse(color);
|
|
const hsl = rgbToHSL(rgb);
|
|
colorParseCache.set(color, hsl);
|
|
return hsl;
|
|
}
|
|
const rgbCacheKeys = ["r", "g", "b", "a"];
|
|
const themeCacheKeys = [
|
|
"mode",
|
|
"brightness",
|
|
"contrast",
|
|
"grayscale",
|
|
"sepia",
|
|
"darkSchemeBackgroundColor",
|
|
"darkSchemeTextColor",
|
|
"lightSchemeBackgroundColor",
|
|
"lightSchemeTextColor"
|
|
];
|
|
function getCacheId(rgb, theme) {
|
|
let resultId = "";
|
|
rgbCacheKeys.forEach((key) => {
|
|
resultId += `${rgb[key]};`;
|
|
});
|
|
themeCacheKeys.forEach((key) => {
|
|
resultId += `${theme[key]};`;
|
|
});
|
|
return resultId;
|
|
}
|
|
function modifyColorWithCache(
|
|
rgb,
|
|
theme,
|
|
modifyHSL,
|
|
poleColor,
|
|
anotherPoleColor
|
|
) {
|
|
let fnCache;
|
|
if (colorModificationCache.has(modifyHSL)) {
|
|
fnCache = colorModificationCache.get(modifyHSL);
|
|
} else {
|
|
fnCache = new Map();
|
|
colorModificationCache.set(modifyHSL, fnCache);
|
|
}
|
|
const id = getCacheId(rgb, theme);
|
|
if (fnCache.has(id)) {
|
|
return fnCache.get(id);
|
|
}
|
|
const hsl = rgbToHSL(rgb);
|
|
const pole = poleColor == null ? null : parseToHSLWithCache(poleColor);
|
|
const anotherPole =
|
|
anotherPoleColor == null
|
|
? null
|
|
: parseToHSLWithCache(anotherPoleColor);
|
|
const modified = modifyHSL(hsl, pole, anotherPole);
|
|
const {r, g, b, a} = hslToRGB(modified);
|
|
const matrix = createFilterMatrix(theme);
|
|
const [rf, gf, bf] = applyColorMatrix([r, g, b], matrix);
|
|
const color =
|
|
a === 1
|
|
? rgbToHexString({r: rf, g: gf, b: bf})
|
|
: rgbToString({r: rf, g: gf, b: bf, a});
|
|
fnCache.set(id, color);
|
|
return color;
|
|
}
|
|
function modifyLightSchemeColor(rgb, theme) {
|
|
const poleBg = getBgPole(theme);
|
|
const poleFg = getFgPole(theme);
|
|
return modifyColorWithCache(
|
|
rgb,
|
|
theme,
|
|
modifyLightModeHSL,
|
|
poleFg,
|
|
poleBg
|
|
);
|
|
}
|
|
function modifyLightModeHSL({h, s, l, a}, poleFg, poleBg) {
|
|
const isDark = l < 0.5;
|
|
let isNeutral;
|
|
if (isDark) {
|
|
isNeutral = l < 0.2 || s < 0.12;
|
|
} else {
|
|
const isBlue = h > 200 && h < 280;
|
|
isNeutral = s < 0.24 || (l > 0.8 && isBlue);
|
|
}
|
|
let hx = h;
|
|
let sx = l;
|
|
if (isNeutral) {
|
|
if (isDark) {
|
|
hx = poleFg.h;
|
|
sx = poleFg.s;
|
|
} else {
|
|
hx = poleBg.h;
|
|
sx = poleBg.s;
|
|
}
|
|
}
|
|
const lx = scale(l, 0, 1, poleFg.l, poleBg.l);
|
|
return {h: hx, s: sx, l: lx, a};
|
|
}
|
|
const MAX_BG_LIGHTNESS = 0.4;
|
|
function modifyBgHSL({h, s, l, a}, pole) {
|
|
const isDark = l < 0.5;
|
|
const isBlue = h > 200 && h < 280;
|
|
const isNeutral = s < 0.12 || (l > 0.8 && isBlue);
|
|
if (isDark) {
|
|
const lx = scale(l, 0, 0.5, 0, MAX_BG_LIGHTNESS);
|
|
if (isNeutral) {
|
|
const hx = pole.h;
|
|
const sx = pole.s;
|
|
return {h: hx, s: sx, l: lx, a};
|
|
}
|
|
return {h, s, l: lx, a};
|
|
}
|
|
const lx = scale(l, 0.5, 1, MAX_BG_LIGHTNESS, pole.l);
|
|
if (isNeutral) {
|
|
const hx = pole.h;
|
|
const sx = pole.s;
|
|
return {h: hx, s: sx, l: lx, a};
|
|
}
|
|
let hx = h;
|
|
const isYellow = h > 60 && h < 180;
|
|
if (isYellow) {
|
|
const isCloserToGreen = h > 120;
|
|
if (isCloserToGreen) {
|
|
hx = scale(h, 120, 180, 135, 180);
|
|
} else {
|
|
hx = scale(h, 60, 120, 60, 105);
|
|
}
|
|
}
|
|
return {h: hx, s, l: lx, a};
|
|
}
|
|
function modifyBackgroundColor(rgb, theme) {
|
|
if (theme.mode === 0) {
|
|
return modifyLightSchemeColor(rgb, theme);
|
|
}
|
|
const pole = getBgPole(theme);
|
|
return modifyColorWithCache(
|
|
rgb,
|
|
{...theme, mode: 0},
|
|
modifyBgHSL,
|
|
pole
|
|
);
|
|
}
|
|
const MIN_FG_LIGHTNESS = 0.55;
|
|
function modifyBlueFgHue(hue) {
|
|
return scale(hue, 205, 245, 205, 220);
|
|
}
|
|
function modifyFgHSL({h, s, l, a}, pole) {
|
|
const isLight = l > 0.5;
|
|
const isNeutral = l < 0.2 || s < 0.24;
|
|
const isBlue = !isNeutral && h > 205 && h < 245;
|
|
if (isLight) {
|
|
const lx = scale(l, 0.5, 1, MIN_FG_LIGHTNESS, pole.l);
|
|
if (isNeutral) {
|
|
const hx = pole.h;
|
|
const sx = pole.s;
|
|
return {h: hx, s: sx, l: lx, a};
|
|
}
|
|
let hx = h;
|
|
if (isBlue) {
|
|
hx = modifyBlueFgHue(h);
|
|
}
|
|
return {h: hx, s, l: lx, a};
|
|
}
|
|
if (isNeutral) {
|
|
const hx = pole.h;
|
|
const sx = pole.s;
|
|
const lx = scale(l, 0, 0.5, pole.l, MIN_FG_LIGHTNESS);
|
|
return {h: hx, s: sx, l: lx, a};
|
|
}
|
|
let hx = h;
|
|
let lx;
|
|
if (isBlue) {
|
|
hx = modifyBlueFgHue(h);
|
|
lx = scale(l, 0, 0.5, pole.l, Math.min(1, MIN_FG_LIGHTNESS + 0.05));
|
|
} else {
|
|
lx = scale(l, 0, 0.5, pole.l, MIN_FG_LIGHTNESS);
|
|
}
|
|
return {h: hx, s, l: lx, a};
|
|
}
|
|
function modifyForegroundColor(rgb, theme) {
|
|
if (theme.mode === 0) {
|
|
return modifyLightSchemeColor(rgb, theme);
|
|
}
|
|
const pole = getFgPole(theme);
|
|
return modifyColorWithCache(
|
|
rgb,
|
|
{...theme, mode: 0},
|
|
modifyFgHSL,
|
|
pole
|
|
);
|
|
}
|
|
function modifyBorderHSL({h, s, l, a}, poleFg, poleBg) {
|
|
const isDark = l < 0.5;
|
|
const isNeutral = l < 0.2 || s < 0.24;
|
|
let hx = h;
|
|
let sx = s;
|
|
if (isNeutral) {
|
|
if (isDark) {
|
|
hx = poleFg.h;
|
|
sx = poleFg.s;
|
|
} else {
|
|
hx = poleBg.h;
|
|
sx = poleBg.s;
|
|
}
|
|
}
|
|
const lx = scale(l, 0, 1, 0.5, 0.2);
|
|
return {h: hx, s: sx, l: lx, a};
|
|
}
|
|
function modifyBorderColor(rgb, theme) {
|
|
if (theme.mode === 0) {
|
|
return modifyLightSchemeColor(rgb, theme);
|
|
}
|
|
const poleFg = getFgPole(theme);
|
|
const poleBg = getBgPole(theme);
|
|
return modifyColorWithCache(
|
|
rgb,
|
|
{...theme, mode: 0},
|
|
modifyBorderHSL,
|
|
poleFg,
|
|
poleBg
|
|
);
|
|
}
|
|
|
|
const themeColorTypes = {
|
|
accentcolor: "bg",
|
|
button_background_active: "text",
|
|
button_background_hover: "text",
|
|
frame: "bg",
|
|
icons: "text",
|
|
icons_attention: "text",
|
|
ntp_background: "bg",
|
|
ntp_text: "text",
|
|
popup: "bg",
|
|
popup_border: "bg",
|
|
popup_highlight: "bg",
|
|
popup_highlight_text: "text",
|
|
popup_text: "text",
|
|
sidebar: "bg",
|
|
sidebar_border: "border",
|
|
sidebar_text: "text",
|
|
tab_background_text: "text",
|
|
tab_line: "bg",
|
|
tab_loading: "bg",
|
|
tab_selected: "bg",
|
|
textcolor: "text",
|
|
toolbar: "bg",
|
|
toolbar_bottom_separator: "border",
|
|
toolbar_field: "bg",
|
|
toolbar_field_border: "border",
|
|
toolbar_field_border_focus: "border",
|
|
toolbar_field_focus: "bg",
|
|
toolbar_field_separator: "border",
|
|
toolbar_field_text: "text",
|
|
toolbar_field_text_focus: "text",
|
|
toolbar_text: "text",
|
|
toolbar_top_separator: "border",
|
|
toolbar_vertical_separator: "border"
|
|
};
|
|
const $colors = {
|
|
accentcolor: "#111111",
|
|
frame: "#111111",
|
|
ntp_background: "white",
|
|
ntp_text: "black",
|
|
popup: "#cccccc",
|
|
popup_text: "black",
|
|
sidebar: "#cccccc",
|
|
sidebar_border: "#333",
|
|
sidebar_text: "black",
|
|
tab_background_text: "white",
|
|
tab_loading: "#23aeff",
|
|
textcolor: "white",
|
|
toolbar: "#707070",
|
|
toolbar_field: "lightgray",
|
|
toolbar_field_text: "black"
|
|
};
|
|
function setWindowTheme(filter) {
|
|
const colors = Object.entries($colors).reduce((obj, [key, value]) => {
|
|
const type = themeColorTypes[key];
|
|
const modify = {
|
|
bg: modifyBackgroundColor,
|
|
text: modifyForegroundColor,
|
|
border: modifyBorderColor
|
|
}[type];
|
|
const rgb = parse(value);
|
|
const modified = modify(rgb, filter);
|
|
obj[key] = modified;
|
|
return obj;
|
|
}, {});
|
|
if (
|
|
typeof browser !== "undefined" &&
|
|
browser.theme &&
|
|
browser.theme.update
|
|
) {
|
|
browser.theme.update({colors});
|
|
}
|
|
}
|
|
function resetWindowTheme() {
|
|
if (
|
|
typeof browser !== "undefined" &&
|
|
browser.theme &&
|
|
browser.theme.reset
|
|
) {
|
|
browser.theme.reset();
|
|
}
|
|
}
|
|
|
|
function createSVGFilterStylesheet(config, url, frameURL, fixes, index) {
|
|
let filterValue;
|
|
let reverseFilterValue;
|
|
if (isFirefox) {
|
|
filterValue = getEmbeddedSVGFilterValue(
|
|
getSVGFilterMatrixValue(config)
|
|
);
|
|
reverseFilterValue = getEmbeddedSVGFilterValue(
|
|
getSVGReverseFilterMatrixValue()
|
|
);
|
|
} else {
|
|
filterValue = "url(#dark-reader-filter)";
|
|
reverseFilterValue = "url(#dark-reader-reverse-filter)";
|
|
}
|
|
return cssFilterStyleSheetTemplate(
|
|
filterValue,
|
|
reverseFilterValue,
|
|
config,
|
|
url,
|
|
frameURL,
|
|
fixes,
|
|
index
|
|
);
|
|
}
|
|
function getEmbeddedSVGFilterValue(matrixValue) {
|
|
const id = "dark-reader-filter";
|
|
const svg = [
|
|
'<svg xmlns="http://www.w3.org/2000/svg">',
|
|
`<filter id="${id}" style="color-interpolation-filters: sRGB;">`,
|
|
`<feColorMatrix type="matrix" values="${matrixValue}" />`,
|
|
"</filter>",
|
|
"</svg>"
|
|
].join("");
|
|
return `url(data:image/svg+xml;base64,${btoa(svg)}#${id})`;
|
|
}
|
|
function toSVGMatrix(matrix) {
|
|
return matrix
|
|
.slice(0, 4)
|
|
.map((m) => m.map((m) => m.toFixed(3)).join(" "))
|
|
.join(" ");
|
|
}
|
|
function getSVGFilterMatrixValue(config) {
|
|
return toSVGMatrix(createFilterMatrix(config));
|
|
}
|
|
function getSVGReverseFilterMatrixValue() {
|
|
return toSVGMatrix(Matrix.invertNHue());
|
|
}
|
|
|
|
const matchesMediaQuery = (query) => {
|
|
if ("window" in globalThis) {
|
|
return Boolean(window.matchMedia(query).matches);
|
|
}
|
|
return false;
|
|
};
|
|
const matchesDarkTheme = () =>
|
|
matchesMediaQuery("(prefers-color-scheme: dark)");
|
|
const matchesLightTheme = () =>
|
|
matchesMediaQuery("(prefers-color-scheme: light)");
|
|
const isColorSchemeSupported = matchesDarkTheme() || matchesLightTheme();
|
|
function isSystemDarkModeEnabled() {
|
|
if (!isColorSchemeSupported) {
|
|
return false;
|
|
}
|
|
return matchesDarkTheme();
|
|
}
|
|
|
|
class Extension {
|
|
constructor() {
|
|
this.isEnabled = null;
|
|
this.wasEnabledOnLastCheck = null;
|
|
this.registeredContextMenus = null;
|
|
this.popupOpeningListener = null;
|
|
this.wasLastColorSchemeDark = null;
|
|
this.startBarrier = null;
|
|
this.stateManager = null;
|
|
this.alarmListener = (alarm) => {
|
|
if (alarm.name === Extension.ALARM_NAME) {
|
|
this.handleAutoCheck();
|
|
}
|
|
};
|
|
this.onCommand = async (command, frameURL) => {
|
|
if (this.startBarrier.isPending()) {
|
|
await this.startBarrier.entry();
|
|
}
|
|
this.stateManager.loadState();
|
|
switch (command) {
|
|
case "toggle":
|
|
this.changeSettings({
|
|
enabled: !this.isEnabled,
|
|
automation: ""
|
|
});
|
|
break;
|
|
case "addSite":
|
|
const url =
|
|
frameURL || (await this.tabs.getActiveTabURL());
|
|
if (isPDF(url)) {
|
|
this.changeSettings({
|
|
enableForPDF: !this.user.settings.enableForPDF
|
|
});
|
|
} else {
|
|
this.toggleURL(url);
|
|
}
|
|
break;
|
|
case "switchEngine": {
|
|
const engines = Object.values(ThemeEngines);
|
|
const index = engines.indexOf(
|
|
this.user.settings.theme.engine
|
|
);
|
|
const next = engines[(index + 1) % engines.length];
|
|
this.setTheme({engine: next});
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
this.onColorSchemeChange = ({isDark}) => {
|
|
if (isFirefox) {
|
|
this.wasLastColorSchemeDark = isDark;
|
|
}
|
|
if (this.user.settings.automation !== "system") {
|
|
return;
|
|
}
|
|
this.handleAutoCheck();
|
|
};
|
|
this.getTabMessage = (url, frameURL) => {
|
|
const urlInfo = this.getURLInfo(url);
|
|
if (
|
|
this.isEnabled &&
|
|
isURLEnabled(url, this.user.settings, urlInfo)
|
|
) {
|
|
const custom = this.user.settings.customThemes.find(
|
|
({url: urlList}) => isURLInList(url, urlList)
|
|
);
|
|
const preset = custom
|
|
? null
|
|
: this.user.settings.presets.find(({urls}) =>
|
|
isURLInList(url, urls)
|
|
);
|
|
const theme = custom
|
|
? custom.theme
|
|
: preset
|
|
? preset.theme
|
|
: this.user.settings.theme;
|
|
logInfo(`Custom theme ${
|
|
custom ? "was found" : "was not found"
|
|
}, Preset theme ${preset ? "was found" : "was not found"}
|
|
The theme(${
|
|
custom ? "custom" : preset ? "preset" : "global"
|
|
} settings) used is: ${JSON.stringify(theme)}`);
|
|
switch (theme.engine) {
|
|
case ThemeEngines.cssFilter: {
|
|
return {
|
|
type: MessageType.BG_ADD_CSS_FILTER,
|
|
data: createCSSFilterStyleSheet(
|
|
theme,
|
|
url,
|
|
frameURL,
|
|
this.config.INVERSION_FIXES_RAW,
|
|
this.config.INVERSION_FIXES_INDEX
|
|
)
|
|
};
|
|
}
|
|
case ThemeEngines.svgFilter: {
|
|
if (isFirefox) {
|
|
return {
|
|
type: MessageType.BG_ADD_CSS_FILTER,
|
|
data: createSVGFilterStylesheet(
|
|
theme,
|
|
url,
|
|
frameURL,
|
|
this.config.INVERSION_FIXES_RAW,
|
|
this.config.INVERSION_FIXES_INDEX
|
|
)
|
|
};
|
|
}
|
|
return {
|
|
type: MessageType.BG_ADD_SVG_FILTER,
|
|
data: {
|
|
css: createSVGFilterStylesheet(
|
|
theme,
|
|
url,
|
|
frameURL,
|
|
this.config.INVERSION_FIXES_RAW,
|
|
this.config.INVERSION_FIXES_INDEX
|
|
),
|
|
svgMatrix: getSVGFilterMatrixValue(theme),
|
|
svgReverseMatrix:
|
|
getSVGReverseFilterMatrixValue()
|
|
}
|
|
};
|
|
}
|
|
case ThemeEngines.staticTheme: {
|
|
return {
|
|
type: MessageType.BG_ADD_STATIC_THEME,
|
|
data:
|
|
theme.stylesheet && theme.stylesheet.trim()
|
|
? theme.stylesheet
|
|
: createStaticStylesheet(
|
|
theme,
|
|
url,
|
|
frameURL,
|
|
this.config.STATIC_THEMES_RAW,
|
|
this.config.STATIC_THEMES_INDEX
|
|
)
|
|
};
|
|
}
|
|
case ThemeEngines.dynamicTheme: {
|
|
const filter = {...theme};
|
|
delete filter.engine;
|
|
const fixes = getDynamicThemeFixesFor(
|
|
url,
|
|
frameURL,
|
|
this.config.DYNAMIC_THEME_FIXES_RAW,
|
|
this.config.DYNAMIC_THEME_FIXES_INDEX,
|
|
this.user.settings.enableForPDF
|
|
);
|
|
const isIFrame = frameURL != null;
|
|
return {
|
|
type: MessageType.BG_ADD_DYNAMIC_THEME,
|
|
data: {filter, fixes, isIFrame}
|
|
};
|
|
}
|
|
default: {
|
|
throw new Error(`Unknown engine ${theme.engine}`);
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
type: MessageType.BG_CLEAN_UP
|
|
};
|
|
};
|
|
this.config = new ConfigManager();
|
|
this.devtools = new DevTools(this.config, async () =>
|
|
this.onSettingsChanged()
|
|
);
|
|
this.messenger = new Messenger(this.getMessengerAdapter());
|
|
this.news = new Newsmaker((news) => this.onNewsUpdate(news));
|
|
this.tabs = new TabManager({
|
|
getConnectionMessage: ({url, frameURL}) =>
|
|
this.getConnectionMessage(url, frameURL),
|
|
getTabMessage: this.getTabMessage,
|
|
onColorSchemeChange: this.onColorSchemeChange
|
|
});
|
|
this.user = new UserStorage({
|
|
onRemoteSettingsChange: () => this.onRemoteSettingsChange()
|
|
});
|
|
this.startBarrier = new PromiseBarrier();
|
|
this.stateManager = new StateManager(
|
|
Extension.LOCAL_STORAGE_KEY,
|
|
this,
|
|
{
|
|
isEnabled: null,
|
|
wasEnabledOnLastCheck: null,
|
|
registeredContextMenus: null
|
|
}
|
|
);
|
|
chrome.alarms.onAlarm.addListener(this.alarmListener);
|
|
if (chrome.permissions.onRemoved) {
|
|
chrome.permissions.onRemoved.addListener((permissions) => {
|
|
if (!permissions.permissions.includes("contextMenus")) {
|
|
this.registeredContextMenus = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
recalculateIsEnabled() {
|
|
if (!this.user.settings) {
|
|
return false;
|
|
}
|
|
const {automation} = this.user.settings;
|
|
let nextCheck;
|
|
switch (automation) {
|
|
case "time":
|
|
this.isEnabled = isInTimeIntervalLocal(
|
|
this.user.settings.time.activation,
|
|
this.user.settings.time.deactivation
|
|
);
|
|
nextCheck = nextTimeInterval(
|
|
this.user.settings.time.activation,
|
|
this.user.settings.time.deactivation
|
|
);
|
|
break;
|
|
case "system":
|
|
if (isMV3) {
|
|
this.isEnabled = true;
|
|
break;
|
|
}
|
|
if (isFirefox) {
|
|
this.isEnabled =
|
|
this.wasLastColorSchemeDark == null
|
|
? isSystemDarkModeEnabled()
|
|
: this.wasLastColorSchemeDark;
|
|
} else {
|
|
this.isEnabled = isSystemDarkModeEnabled();
|
|
}
|
|
break;
|
|
case "location": {
|
|
const {latitude, longitude} = this.user.settings.location;
|
|
if (latitude != null && longitude != null) {
|
|
this.isEnabled = isNightAtLocation(latitude, longitude);
|
|
nextCheck = nextTimeChangeAtLocation(
|
|
latitude,
|
|
longitude
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
this.isEnabled = this.user.settings.enabled;
|
|
break;
|
|
}
|
|
if (nextCheck) {
|
|
chrome.alarms.create(Extension.ALARM_NAME, {when: nextCheck});
|
|
}
|
|
return this.isEnabled;
|
|
}
|
|
async start() {
|
|
await this.config.load({local: true});
|
|
await this.user.loadSettings();
|
|
if (
|
|
this.user.settings.enableContextMenus &&
|
|
!this.registeredContextMenus
|
|
) {
|
|
chrome.permissions.contains(
|
|
{permissions: ["contextMenus"]},
|
|
(permitted) => {
|
|
if (permitted) {
|
|
this.registerContextMenus();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
if (this.user.settings.syncSitesFixes) {
|
|
await this.config.load({local: false});
|
|
}
|
|
this.onAppToggle();
|
|
logInfo("loaded", this.user.settings);
|
|
if (isThunderbird) {
|
|
this.tabs.registerMailDisplayScript();
|
|
} else {
|
|
this.tabs.updateContentScript({
|
|
runOnProtectedPages:
|
|
this.user.settings.enableForProtectedPages
|
|
});
|
|
}
|
|
this.user.settings.fetchNews && this.news.subscribe();
|
|
this.startBarrier.resolve();
|
|
}
|
|
getMessengerAdapter() {
|
|
return {
|
|
collect: async () => {
|
|
return await this.collectData();
|
|
},
|
|
getActiveTabInfo: async () => {
|
|
if (!this.user.settings) {
|
|
await this.user.loadSettings();
|
|
}
|
|
await this.stateManager.loadState();
|
|
const url = await this.tabs.getActiveTabURL();
|
|
const info = this.getURLInfo(url);
|
|
info.isInjected = await this.tabs.canAccessActiveTab();
|
|
return info;
|
|
},
|
|
changeSettings: (settings) => this.changeSettings(settings),
|
|
setTheme: (theme) => this.setTheme(theme),
|
|
setShortcut: ({command, shortcut}) =>
|
|
this.setShortcut(command, shortcut),
|
|
toggleURL: (url) => this.toggleURL(url),
|
|
markNewsAsRead: async (ids) =>
|
|
await this.news.markAsRead(...ids),
|
|
onPopupOpen: () =>
|
|
this.popupOpeningListener && this.popupOpeningListener(),
|
|
loadConfig: async (options) => await this.config.load(options),
|
|
applyDevDynamicThemeFixes: (text) =>
|
|
this.devtools.applyDynamicThemeFixes(text),
|
|
resetDevDynamicThemeFixes: () =>
|
|
this.devtools.resetDynamicThemeFixes(),
|
|
applyDevInversionFixes: (text) =>
|
|
this.devtools.applyInversionFixes(text),
|
|
resetDevInversionFixes: () =>
|
|
this.devtools.resetInversionFixes(),
|
|
applyDevStaticThemes: (text) =>
|
|
this.devtools.applyStaticThemes(text),
|
|
resetDevStaticThemes: () => this.devtools.resetStaticThemes()
|
|
};
|
|
}
|
|
registerContextMenus() {
|
|
const onCommandToggle = async () => this.onCommand("toggle");
|
|
const onCommandAddSite = async (data) =>
|
|
this.onCommand("addSite", data.frameUrl);
|
|
const onCommandSwitchEngine = async () =>
|
|
this.onCommand("switchEngine");
|
|
chrome.contextMenus.removeAll(() => {
|
|
this.registeredContextMenus = false;
|
|
chrome.contextMenus.create(
|
|
{
|
|
id: "DarkReader-top",
|
|
title: "Dark Reader"
|
|
},
|
|
() => {
|
|
if (chrome.runtime.lastError) {
|
|
return;
|
|
}
|
|
const msgToggle =
|
|
chrome.i18n.getMessage("toggle_extension");
|
|
const msgAddSite = chrome.i18n.getMessage(
|
|
"toggle_current_site"
|
|
);
|
|
const msgSwitchEngine = chrome.i18n.getMessage(
|
|
"theme_generation_mode"
|
|
);
|
|
chrome.contextMenus.create({
|
|
id: "DarkReader-toggle",
|
|
parentId: "DarkReader-top",
|
|
title: msgToggle || "Toggle everywhere",
|
|
onclick: onCommandToggle
|
|
});
|
|
chrome.contextMenus.create({
|
|
id: "DarkReader-addSite",
|
|
parentId: "DarkReader-top",
|
|
title: msgAddSite || "Toggle for current site",
|
|
onclick: onCommandAddSite
|
|
});
|
|
chrome.contextMenus.create({
|
|
id: "DarkReader-switchEngine",
|
|
parentId: "DarkReader-top",
|
|
title: msgSwitchEngine || "Switch engine",
|
|
onclick: onCommandSwitchEngine
|
|
});
|
|
this.registeredContextMenus = true;
|
|
}
|
|
);
|
|
});
|
|
}
|
|
async getShortcuts() {
|
|
const commands = await getCommands();
|
|
return commands.reduce(
|
|
(map, cmd) => Object.assign(map, {[cmd.name]: cmd.shortcut}),
|
|
{}
|
|
);
|
|
}
|
|
setShortcut(command, shortcut) {
|
|
setShortcut(command, shortcut);
|
|
}
|
|
async collectData() {
|
|
if (!this.user.settings) {
|
|
await this.user.loadSettings();
|
|
}
|
|
await this.stateManager.loadState();
|
|
return {
|
|
isEnabled: this.isEnabled,
|
|
isReady: true,
|
|
settings: this.user.settings,
|
|
news: await this.news.getLatest(),
|
|
shortcuts: await this.getShortcuts(),
|
|
colorScheme: this.config.COLOR_SCHEMES_RAW,
|
|
devtools: {
|
|
dynamicFixesText:
|
|
await this.devtools.getDynamicThemeFixesText(),
|
|
filterFixesText:
|
|
await this.devtools.getInversionFixesText(),
|
|
staticThemesText: await this.devtools.getStaticThemesText(),
|
|
hasCustomDynamicFixes:
|
|
await this.devtools.hasCustomDynamicThemeFixes(),
|
|
hasCustomFilterFixes:
|
|
await this.devtools.hasCustomFilterFixes(),
|
|
hasCustomStaticFixes:
|
|
await this.devtools.hasCustomStaticFixes()
|
|
}
|
|
};
|
|
}
|
|
onNewsUpdate(news) {
|
|
if (!this.icon) {
|
|
this.icon = new IconManager();
|
|
}
|
|
const latestNews = news.length > 0 && news[0];
|
|
if (latestNews && latestNews.important && !latestNews.read) {
|
|
this.icon.showImportantBadge();
|
|
return;
|
|
}
|
|
this.icon.hideBadge();
|
|
}
|
|
getConnectionMessage(url, frameURL) {
|
|
if (this.user.settings) {
|
|
return this.getTabMessage(url, frameURL);
|
|
}
|
|
return new Promise((resolve) => {
|
|
this.user
|
|
.loadSettings()
|
|
.then(() => resolve(this.getTabMessage(url, frameURL)));
|
|
});
|
|
}
|
|
async handleAutoCheck() {
|
|
if (!this.user.settings) {
|
|
await this.user.loadSettings();
|
|
}
|
|
await this.stateManager.loadState();
|
|
this.recalculateIsEnabled();
|
|
const isEnabled = this.isEnabled;
|
|
if (
|
|
this.wasEnabledOnLastCheck === null ||
|
|
this.wasEnabledOnLastCheck !== isEnabled
|
|
) {
|
|
this.wasEnabledOnLastCheck = isEnabled;
|
|
this.onAppToggle();
|
|
this.tabs.sendMessage();
|
|
this.reportChanges();
|
|
this.stateManager.saveState();
|
|
}
|
|
}
|
|
changeSettings($settings) {
|
|
const prev = {...this.user.settings};
|
|
this.user.set($settings);
|
|
if (
|
|
prev.enabled !== this.user.settings.enabled ||
|
|
prev.automation !== this.user.settings.automation ||
|
|
prev.time.activation !== this.user.settings.time.activation ||
|
|
prev.time.deactivation !==
|
|
this.user.settings.time.deactivation ||
|
|
prev.location.latitude !==
|
|
this.user.settings.location.latitude ||
|
|
prev.location.longitude !==
|
|
this.user.settings.location.longitude
|
|
) {
|
|
this.onAppToggle();
|
|
}
|
|
if (prev.syncSettings !== this.user.settings.syncSettings) {
|
|
this.user.saveSyncSetting(this.user.settings.syncSettings);
|
|
}
|
|
if (
|
|
this.isEnabled &&
|
|
$settings.changeBrowserTheme != null &&
|
|
prev.changeBrowserTheme !== $settings.changeBrowserTheme
|
|
) {
|
|
if ($settings.changeBrowserTheme) {
|
|
setWindowTheme(this.user.settings.theme);
|
|
} else {
|
|
resetWindowTheme();
|
|
}
|
|
}
|
|
if (prev.fetchNews !== this.user.settings.fetchNews) {
|
|
this.user.settings.fetchNews
|
|
? this.news.subscribe()
|
|
: this.news.unSubscribe();
|
|
}
|
|
if (
|
|
prev.enableContextMenus !==
|
|
this.user.settings.enableContextMenus
|
|
) {
|
|
if (this.user.settings.enableContextMenus) {
|
|
this.registerContextMenus();
|
|
} else {
|
|
chrome.contextMenus.removeAll();
|
|
}
|
|
}
|
|
this.onSettingsChanged();
|
|
}
|
|
setTheme($theme) {
|
|
this.user.set({theme: {...this.user.settings.theme, ...$theme}});
|
|
if (this.isEnabled && this.user.settings.changeBrowserTheme) {
|
|
setWindowTheme(this.user.settings.theme);
|
|
}
|
|
this.onSettingsChanged();
|
|
}
|
|
async reportChanges() {
|
|
const info = await this.collectData();
|
|
this.messenger.reportChanges(info);
|
|
}
|
|
toggleURL(url) {
|
|
const isInDarkList = isURLInList(url, this.config.DARK_SITES);
|
|
const siteList = isInDarkList
|
|
? this.user.settings.siteListEnabled.slice()
|
|
: this.user.settings.siteList.slice();
|
|
const pattern = getURLHostOrProtocol(url);
|
|
const index = siteList.indexOf(pattern);
|
|
if (index < 0) {
|
|
siteList.push(pattern);
|
|
} else {
|
|
siteList.splice(index, 1);
|
|
}
|
|
if (isInDarkList) {
|
|
this.changeSettings({siteListEnabled: siteList});
|
|
} else {
|
|
this.changeSettings({siteList});
|
|
}
|
|
}
|
|
async toggleCurrentSite() {
|
|
const url = await this.tabs.getActiveTabURL();
|
|
this.toggleURL(url);
|
|
}
|
|
onAppToggle() {
|
|
if (!this.icon) {
|
|
this.icon = new IconManager();
|
|
}
|
|
this.recalculateIsEnabled();
|
|
if (this.isEnabled) {
|
|
this.icon.setActive();
|
|
if (this.user.settings.changeBrowserTheme) {
|
|
setWindowTheme(this.user.settings.theme);
|
|
}
|
|
} else {
|
|
this.icon.setInactive();
|
|
if (this.user.settings.changeBrowserTheme) {
|
|
resetWindowTheme();
|
|
}
|
|
}
|
|
}
|
|
async onSettingsChanged() {
|
|
if (!this.user.settings) {
|
|
await this.user.loadSettings();
|
|
}
|
|
await this.stateManager.loadState();
|
|
this.wasEnabledOnLastCheck = this.isEnabled;
|
|
this.tabs.sendMessage();
|
|
this.saveUserSettings();
|
|
this.reportChanges();
|
|
this.stateManager.saveState();
|
|
}
|
|
onRemoteSettingsChange() {}
|
|
getURLInfo(url) {
|
|
const {DARK_SITES} = this.config;
|
|
const isInDarkList = isURLInList(url, DARK_SITES);
|
|
const isProtected = !canInjectScript(url);
|
|
return {
|
|
url,
|
|
isInDarkList,
|
|
isProtected,
|
|
isInjected: null
|
|
};
|
|
}
|
|
async saveUserSettings() {
|
|
await this.user.saveSettings();
|
|
logInfo("saved", this.user.settings);
|
|
}
|
|
}
|
|
Extension.ALARM_NAME = "auto-time-alarm";
|
|
Extension.LOCAL_STORAGE_KEY = "Extension-state";
|
|
|
|
function makeChromiumHappy() {
|
|
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
|
|
if (
|
|
![
|
|
MessageType.CS_FRAME_CONNECT,
|
|
MessageType.UI_GET_DATA,
|
|
MessageType.UI_GET_ACTIVE_TAB_INFO,
|
|
MessageType.UI_APPLY_DEV_DYNAMIC_THEME_FIXES,
|
|
MessageType.UI_APPLY_DEV_INVERSION_FIXES,
|
|
MessageType.UI_APPLY_DEV_STATIC_THEMES
|
|
].includes(message.type)
|
|
) {
|
|
sendResponse({type: "¯\\_(ツ)_/¯"});
|
|
}
|
|
});
|
|
}
|
|
|
|
const extension = new Extension();
|
|
extension.start();
|
|
if (chrome.commands) {
|
|
chrome.commands.onCommand.addListener(async (command) =>
|
|
extension.onCommand(command)
|
|
);
|
|
}
|
|
const welcome = ` /''''\\
|
|
(0)==(0)
|
|
/__||||__\\
|
|
Welcome to Dark Reader!`;
|
|
console.log(welcome);
|
|
{
|
|
chrome.runtime.onInstalled.addListener(({reason}) => {
|
|
if (reason === "install") {
|
|
chrome.tabs.create({url: getHelpURL()});
|
|
}
|
|
});
|
|
chrome.runtime.setUninstallURL(UNINSTALL_URL);
|
|
}
|
|
makeChromiumHappy();
|
|
})();
|