.dotfiles/.config/chromium/Default/Extensions/eimadpbcbfnmbkopoojfekhnkhdbieeh/4.9.92_0/background/index.js

6854 lines
249 KiB
JavaScript
Raw Normal View History

2024-09-21 14:05:15 -04:00
(function () {
"use strict";
const isNavigatorDefined = typeof navigator !== "undefined";
const userAgent = isNavigatorDefined
? navigator.userAgentData &&
Array.isArray(navigator.userAgentData.brands)
? navigator.userAgentData.brands
.map(
(brand) => `${brand.brand.toLowerCase()} ${brand.version}`
)
.join(" ")
: navigator.userAgent.toLowerCase()
: "some useragent";
const platform = isNavigatorDefined
? navigator.userAgentData &&
typeof navigator.userAgentData.platform === "string"
? navigator.userAgentData.platform.toLowerCase()
: navigator.platform.toLowerCase()
: "some platform";
userAgent.includes("vivaldi");
userAgent.includes("yabrowser");
const isOpera = userAgent.includes("opr") || userAgent.includes("opera");
const isEdge = userAgent.includes("edg");
const isWindows = platform.startsWith("win");
const isMacOS = platform.startsWith("mac");
const isMobile =
isNavigatorDefined && navigator.userAgentData
? navigator.userAgentData.mobile
: userAgent.includes("mobile");
(isNavigatorDefined &&
navigator.userAgentData &&
["Linux", "Android"].includes(navigator.userAgentData.platform)) ||
platform.startsWith("linux");
(() => {
const m = userAgent.match(/chrom(?:e|ium)(?:\/| )([^ ]+)/);
if (m && m[1]) {
return m[1];
}
return "";
})();
(() => {
const m = userAgent.match(/(?:firefox|librewolf)(?:\/| )([^ ]+)/);
if (m && m[1]) {
return m[1];
}
return "";
})();
(() => {
try {
document.querySelector(":defined");
return true;
} catch (err) {
return false;
}
})();
const isXMLHttpRequestSupported = typeof XMLHttpRequest === "function";
const isFetchSupported = typeof fetch === "function";
async function getOKResponse(url, mimeType, origin) {
const response = await fetch(url, {
cache: "force-cache",
credentials: "omit",
referrer: origin
});
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 getParenthesesRange(input, searchStartIndex = 0) {
return getOpenCloseRange(input, searchStartIndex, "(", ")", []);
}
function getOpenCloseRange(
input,
searchStartIndex,
openToken,
closeToken,
excludeRanges
) {
let indexOf;
if (excludeRanges.length === 0) {
indexOf = (token, pos) => input.indexOf(token, pos);
} else {
indexOf = (token, pos) =>
indexOfExcluding(input, token, pos, excludeRanges);
}
const {length} = input;
let depth = 0;
let firstOpenIndex = -1;
for (let i = searchStartIndex; i < length; i++) {
if (depth === 0) {
const openIndex = indexOf(openToken, i);
if (openIndex < 0) {
break;
}
firstOpenIndex = openIndex;
depth++;
i = openIndex;
} else {
const closeIndex = indexOf(closeToken, i);
if (closeIndex < 0) {
break;
}
const openIndex = indexOf(openToken, i);
if (openIndex < 0 || closeIndex <= openIndex) {
depth--;
if (depth === 0) {
return {start: firstOpenIndex, end: closeIndex + 1};
}
i = closeIndex;
} else {
depth++;
i = openIndex;
}
}
}
return null;
}
function indexOfExcluding(input, search, position, excludeRanges) {
const i = input.indexOf(search, position);
const exclusion = excludeRanges.find((r) => i >= r.start && i < r.end);
if (exclusion) {
return indexOfExcluding(
input,
search,
exclusion.end,
excludeRanges
);
}
return i;
}
function splitExcluding(input, separator, excludeRanges) {
const parts = [];
let commaIndex = -1;
let currIndex = 0;
while (
(commaIndex = indexOfExcluding(
input,
separator,
currIndex,
excludeRanges
)) >= 0
) {
parts.push(input.substring(currIndex, commaIndex).trim());
currIndex = commaIndex + 1;
}
parts.push(input.substring(currIndex).trim());
return parts;
}
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]);
date.setSeconds(0);
date.setMilliseconds(0);
return date.getTime();
}
if (compareTime(t, b) < 0) {
date.setHours(b[0]);
date.setMinutes(b[1]);
date.setSeconds(0);
date.setMilliseconds(0);
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,
alwaysNight: false,
sunriseTime: 0,
sunsetTime: 0
};
} else if (sunriseTime.alwaysNight || sunsetTime.alwaysNight) {
return {
alwaysDay: false,
alwaysNight: true,
sunriseTime: 0,
sunsetTime: 0
};
}
return {
alwaysDay: false,
alwaysNight: false,
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 {
static QUOTA_BYTES = (navigator.deviceMemory || 4) * 16 * 1024 * 1024;
static TTL = getDuration({minutes: 10});
static ALARM_NAME = "network";
bytesInUse = 0;
records = new Map();
static alarmIsActive = false;
constructor() {
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === LimitedCacheStorage.ALARM_NAME) {
LimitedCacheStorage.alarmIsActive = false;
this.removeExpiredRecords();
}
});
}
static 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) {
LimitedCacheStorage.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) {
LimitedCacheStorage.ensureAlarmIsScheduled();
}
}
}
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 cachedFactory(factory, size) {
const cache = new Map();
return (key) => {
if (cache.has(key)) {
return cache.get(key);
}
const value = factory(key);
cache.set(key, value);
if (cache.size > size) {
const first = cache.keys().next().value;
cache.delete(first);
}
return value;
};
}
function getURLHostOrProtocol($url) {
const url = new URL($url);
if (url.host) {
return url.host;
} else if (url.protocol === "file:") {
return url.pathname;
}
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) {
if (isRegExp(urlTemplate)) {
const regexp = createRegExp(urlTemplate);
return regexp ? regexp.test(url) : false;
}
return matchURLPattern(url, urlTemplate);
}
const URL_CACHE_SIZE = 32;
const prepareURL = cachedFactory((url) => {
let parsed;
try {
parsed = new URL(url);
} catch (err) {
return null;
}
const {hostname, pathname, protocol, port} = parsed;
const hostParts = hostname.split(".").reverse();
const pathParts = pathname.split("/").slice(1);
if (!pathParts[pathParts.length - 1]) {
pathParts.splice(pathParts.length - 1, 1);
}
return {
hostParts,
pathParts,
port,
protocol
};
}, URL_CACHE_SIZE);
const URL_MATCH_CACHE_SIZE = 32 * 1024;
const preparePattern = cachedFactory((pattern) => {
if (!pattern) {
return null;
}
const exactStart = pattern.startsWith("^");
const exactEnd = pattern.endsWith("$");
if (exactStart) {
pattern = pattern.substring(1);
}
if (exactEnd) {
pattern = pattern.substring(0, pattern.length - 1);
}
let protocol = "";
const protocolIndex = pattern.indexOf("://");
if (protocolIndex > 0) {
protocol = pattern.substring(0, protocolIndex + 1);
pattern = pattern.substring(protocolIndex + 3);
}
const slashIndex = pattern.indexOf("/");
const host =
slashIndex < 0 ? pattern : pattern.substring(0, slashIndex);
let hostName = host;
let isIPv6 = false;
let ipV6End = -1;
if (host.startsWith("[")) {
ipV6End = host.indexOf("]");
if (ipV6End > 0) {
isIPv6 = true;
}
}
let port = "*";
const portIndex = host.lastIndexOf(":");
if (portIndex >= 0 && (!isIPv6 || ipV6End < portIndex)) {
hostName = host.substring(0, portIndex);
port = host.substring(portIndex + 1);
}
if (isIPv6) {
try {
const ipV6URL = new URL(`http://${hostName}`);
hostName = ipV6URL.hostname;
} catch (err) {}
}
const hostParts = hostName.split(".").reverse();
const path = slashIndex < 0 ? "" : pattern.substring(slashIndex + 1);
const pathParts = path.split("/");
if (!pathParts[pathParts.length - 1]) {
pathParts.splice(pathParts.length - 1, 1);
}
return {
hostParts,
pathParts,
port,
exactStart,
exactEnd,
protocol
};
}, URL_MATCH_CACHE_SIZE);
function matchURLPattern(url, pattern) {
const u = prepareURL(url);
const p = preparePattern(pattern);
if (
!(u && p) ||
p.hostParts.length > u.hostParts.length ||
(p.exactStart && p.hostParts.length !== u.hostParts.length) ||
(p.exactEnd && p.pathParts.length !== u.pathParts.length) ||
(p.port !== "*" && p.port !== u.port) ||
(p.protocol && p.protocol !== u.protocol)
) {
return false;
}
for (let i = 0; i < p.hostParts.length; i++) {
const pHostPart = p.hostParts[i];
const uHostPart = u.hostParts[i];
if (pHostPart !== "*" && pHostPart !== uHostPart) {
return false;
}
}
if (
p.hostParts.length >= 2 &&
p.hostParts.at(-1) !== "*" &&
(p.hostParts.length < u.hostParts.length - 1 ||
(p.hostParts.length === u.hostParts.length - 1 &&
u.hostParts.at(-1) !== "www"))
) {
return false;
}
if (p.pathParts.length === 0) {
return true;
}
if (p.pathParts.length > u.pathParts.length) {
return false;
}
for (let i = 0; i < p.pathParts.length; i++) {
const pPathPart = p.pathParts[i];
const uPathPart = u.pathParts[i];
if (pPathPart !== "*" && pPathPart !== uPathPart) {
return false;
}
}
return true;
}
function isRegExp(pattern) {
return (
pattern.startsWith("/") &&
pattern.endsWith("/") &&
pattern.length > 2
);
}
const REGEXP_CACHE_SIZE = 1024;
const createRegExp = cachedFactory((pattern) => {
if (pattern.startsWith("/")) {
pattern = pattern.substring(1);
}
if (pattern.endsWith("/")) {
pattern = pattern.substring(0, pattern.length - 1);
}
try {
return new RegExp(pattern);
} catch (err) {
return null;
}
}, REGEXP_CACHE_SIZE);
function isPDF(url) {
try {
const {hostname, pathname} = new URL(url);
if (pathname.includes(".pdf")) {
if (
(hostname.match(/(wikipedia|wikimedia)\.org$/i) &&
pathname.match(/^\/.*\/[a-z]+\:[^\:\/]+\.pdf/i)) ||
(hostname.match(/timetravel\.mementoweb\.org$/i) &&
pathname.match(/^\/reconstruct/i) &&
pathname.match(/\.pdf$/i)) ||
(hostname.match(/dropbox\.com$/i) &&
pathname.match(/^\/s\//i) &&
pathname.match(/\.pdf$/i))
) {
return false;
}
if (pathname.endsWith(".pdf")) {
for (let i = pathname.length; i >= 0; i--) {
if (pathname[i] === "=") {
return false;
} else if (pathname[i] === "/") {
return true;
}
}
} else {
return false;
}
}
} catch (e) {}
return false;
}
function isURLEnabled(
url,
userSettings,
{isProtected, isInDarkList, isDarkThemeDetected},
isAllowedFileSchemeAccess = true
) {
if (isLocalFile(url) && !isAllowedFileSchemeAccess) {
return false;
}
if (isProtected && !userSettings.enableForProtectedPages) {
return false;
}
if (isPDF(url)) {
return userSettings.enableForPDF;
}
const isURLInDisabledList = isURLInList(url, userSettings.disabledFor);
const isURLInEnabledList = isURLInList(url, userSettings.enabledFor);
if (!userSettings.enabledByDefault) {
return isURLInEnabledList;
}
if (isURLInEnabledList) {
return true;
}
if (
isInDarkList ||
(userSettings.detectDarkTheme && isDarkThemeDetected)
) {
return false;
}
return !isURLInDisabledList;
}
function isFullyQualifiedDomain(candidate) {
return (
/^[a-z0-9\.\-]+$/i.test(candidate) && candidate.indexOf("..") === -1
);
}
function isFullyQualifiedDomainWildcard(candidate) {
if (!candidate.includes("*") || !/^[a-z0-9\.\-\*]+$/i.test(candidate)) {
return false;
}
const labels = candidate.split(".");
for (const label of labels) {
if (label !== "*" && !/^[a-z0-9\-]+$/i.test(label)) {
return false;
}
}
return true;
}
function fullyQualifiedDomainMatchesWildcard(wildcard, candidate) {
const wildcardLabels = wildcard.toLowerCase().split(".");
const candidateLabels = candidate.toLowerCase().split(".");
if (candidateLabels.length < wildcardLabels.length) {
return false;
}
while (wildcardLabels.length) {
const wildcardLabel = wildcardLabels.pop();
const candidateLabel = candidateLabels.pop();
if (wildcardLabel !== "*" && wildcardLabel !== candidateLabel) {
return false;
}
}
return true;
}
function isLocalFile(url) {
return Boolean(url) && url.startsWith("file:///");
}
const INDEX_CACHE_CLEANUP_INTERVAL_IN_MS = 60000;
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 addLabel(set, label, index) {
if (!set[label]) {
set[label] = [index];
} else if (!set[label].includes(index)) {
set[label].push(index);
}
}
function extractDomainLabelsFromFullyQualifiedDomainWildcard(
fullyQualifiedDomainWildcard
) {
const postfixStart = fullyQualifiedDomainWildcard.lastIndexOf("*");
const postfix = fullyQualifiedDomainWildcard.substring(
postfixStart + 2
);
if (postfixStart < 0 || postfix.length === 0) {
return fullyQualifiedDomainWildcard.split(".");
}
const labels = [postfix];
const prefix = fullyQualifiedDomainWildcard.substring(0, postfixStart);
prefix
.split(".")
.filter(Boolean)
.forEach((l) => labels.concat(l));
return labels;
}
function indexConfigURLs(urls) {
const domains = {};
const domainLabels = {};
const nonstandard = [];
const domainLabelFrequencies = {};
const domainLabelMembers = [];
for (let index = 0; index < urls.length; index++) {
const block = urls[index];
const blockDomainLabels = new Set();
for (const url of block) {
const domain = getDomain(url);
if (isFullyQualifiedDomain(domain)) {
addLabel(domains, domain, index);
} else if (isFullyQualifiedDomainWildcard(domain)) {
const labels =
extractDomainLabelsFromFullyQualifiedDomainWildcard(
domain
);
domainLabelMembers.push({labels, index});
labels.forEach((l) => blockDomainLabels.add(l));
} else {
nonstandard.push(index);
break;
}
}
for (const label of blockDomainLabels) {
if (domainLabelFrequencies[label]) {
domainLabelFrequencies[label]++;
} else {
domainLabelFrequencies[label] = 1;
}
}
}
for (const {labels, index} of domainLabelMembers) {
let label = labels[0];
for (const currLabel of labels) {
if (
domainLabelFrequencies[currLabel] <
domainLabelFrequencies[label]
) {
label = currLabel;
}
}
addLabel(domainLabels, label, index);
}
return {domains, domainLabels, nonstandard};
}
function processSiteFixesConfigBlock(
text,
offsets,
recordStart,
recordEnd,
urls
) {
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;
}
offsets.push([recordStart, recordEnd - recordStart]);
const urls_ = parseArray(lines.slice(0, commandIndices[0]).join("\n"));
urls.push(urls_);
}
function extractURLsFromSiteFixesConfig(text) {
const urls = [];
const offsets = [];
let recordStart = 0;
const delimiterRegex = /^\s*={2,}\s*$/gm;
let delimiter;
while ((delimiter = delimiterRegex.exec(text))) {
const nextDelimiterStart = delimiter.index;
const nextDelimiterEnd = delimiter.index + delimiter[0].length;
processSiteFixesConfigBlock(
text,
offsets,
recordStart,
nextDelimiterStart,
urls
);
recordStart = nextDelimiterEnd;
}
processSiteFixesConfigBlock(
text,
offsets,
recordStart,
text.length,
urls
);
return {urls, offsets};
}
function indexSitesFixesConfig(text) {
const {urls, offsets} = extractURLsFromSiteFixesConfig(text);
const {domains, domainLabels, nonstandard} = indexConfigURLs(urls);
return {
offsets: encodeOffsets(offsets),
domains,
domainLabels,
nonstandard,
cacheDomainIndex: {},
cacheSiteFix: {},
cacheCleanupTimer: null
};
}
function lookupConfigURLsInDomainLabels(
domain,
recordIds,
currRecordIds,
getAllRecordURLs
) {
for (const recordId of currRecordIds) {
const recordURLs = getAllRecordURLs(recordId);
for (const ruleUrl of recordURLs) {
const wildcard = getDomain(ruleUrl);
if (
isFullyQualifiedDomainWildcard(wildcard) &&
fullyQualifiedDomainMatchesWildcard(wildcard, domain)
) {
recordIds.push(recordId);
}
}
}
}
function lookupConfigURLs(domain, index, getAllRecordURLs) {
const labels = domain.split(".");
let recordIds = [];
if (index.domainLabels.hasOwnProperty("*")) {
recordIds = recordIds.concat(index.domainLabels["*"]);
}
for (const label of labels) {
if (index.domainLabels.hasOwnProperty(label)) {
const currRecordIds = index.domainLabels[label];
lookupConfigURLsInDomainLabels(
domain,
recordIds,
currRecordIds,
getAllRecordURLs
);
}
}
for (let i = 0; i < labels.length; i++) {
const substring = labels.slice(i).join(".");
if (index.domains.hasOwnProperty(substring)) {
recordIds = recordIds.concat(index.domains[substring]);
}
if (index.domainLabels.hasOwnProperty(substring)) {
const currRecordIds = index.domainLabels[substring];
lookupConfigURLsInDomainLabels(
domain,
recordIds,
currRecordIds,
getAllRecordURLs
);
}
}
if (index.nonstandard) {
for (const currRecordId of index.nonstandard) {
const urls = getAllRecordURLs(currRecordId);
if (urls.some((url) => isURLMatched(domain, getDomain(url)))) {
recordIds.push(currRecordId);
continue;
}
}
}
recordIds = Array.from(new Set(recordIds));
return recordIds;
}
function getSiteFix(text, index, options, id) {
if (index.cacheSiteFix.hasOwnProperty(id)) {
return index.cacheSiteFix[id];
}
const [blockStart, blockEnd] = decodeOffset(index.offsets, id);
const block = text.substring(blockStart, blockEnd);
const fix = parseSitesFixesConfig(block, options)[0];
index.cacheSiteFix[id] = fix;
return fix;
}
function scheduleCacheCleanup(index) {
clearTimeout(index.cacheCleanupTimer);
index.cacheCleanupTimer = setTimeout(() => {
index.cacheCleanupTimer = null;
index.cacheDomainIndex = {};
index.cacheSiteFix = {};
}, INDEX_CACHE_CLEANUP_INTERVAL_IN_MS);
}
function getSitesFixesFor(url, text, index, options) {
const records = [];
const domain = getDomain(url);
if (!index.cacheDomainIndex[domain]) {
index.cacheDomainIndex[domain] = lookupConfigURLs(
domain,
index,
(recordId) => getSiteFix(text, index, options, recordId).url
);
}
const recordIds = index.cacheDomainIndex[domain];
for (const recordId of recordIds) {
const fix = getSiteFix(text, index, options, recordId);
records.push(fix);
}
scheduleCacheCleanup(index);
return records;
}
function indexSiteListConfig(text) {
const urls = parseArray(text);
const urls2D = urls.map((u) => [u]);
const {domains, domainLabels, nonstandard} = indexConfigURLs(urls2D);
return {domains, domainLabels, nonstandard, urls};
}
function getSiteListFor(url, index) {
const domain = getDomain(url);
const recordIds = lookupConfigURLs(domain, index, (recordId) => [
index.urls[recordId]
]);
const result = [];
for (const recordId of recordIds) {
result.push(index.urls[recordId]);
}
return result;
}
function isURLInSiteList(url, index) {
if (index === null) {
return false;
}
const urls = getSiteListFor(url, index);
return isURLInList(url, urls);
}
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 ThemeEngine;
(function (ThemeEngine) {
ThemeEngine["cssFilter"] = "cssFilter";
ThemeEngine["svgFilter"] = "svgFilter";
ThemeEngine["staticTheme"] = "staticTheme";
ThemeEngine["dynamicTheme"] = "dynamicTheme";
})(ThemeEngine || (ThemeEngine = {}));
var AutomationMode;
(function (AutomationMode) {
AutomationMode["NONE"] = "";
AutomationMode["TIME"] = "time";
AutomationMode["SYSTEM"] = "system";
AutomationMode["LOCATION"] = "location";
})(AutomationMode || (AutomationMode = {}));
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: ThemeEngine.dynamicTheme,
stylesheet: "",
darkSchemeBackgroundColor: DEFAULT_COLORS.darkScheme.background,
darkSchemeTextColor: DEFAULT_COLORS.darkScheme.text,
lightSchemeBackgroundColor: DEFAULT_COLORS.lightScheme.background,
lightSchemeTextColor: DEFAULT_COLORS.lightScheme.text,
scrollbarColor: "",
selectionColor: "auto",
styleSystemControls: false,
lightColorScheme: "Default",
darkColorScheme: "Default",
immediateModify: false
};
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 filterModeSites = [
"*.officeapps.live.com",
"*.sharepoint.com",
"docs.google.com",
"onedrive.live.com"
];
const DEFAULT_SETTINGS = {
schemeVersion: 0,
enabled: true,
fetchNews: true,
theme: DEFAULT_THEME,
presets: [],
customThemes: filterModeSites.map((url) => {
const engine = ThemeEngine.svgFilter;
return {
url: [url],
theme: {...DEFAULT_THEME, engine},
builtIn: true
};
}),
enabledByDefault: true,
enabledFor: [],
disabledFor: [],
changeBrowserTheme: false,
syncSettings: true,
syncSitesFixes: false,
automation: {
enabled: isEdge && isMobile ? true : false,
mode:
isEdge && isMobile
? AutomationMode.SYSTEM
: AutomationMode.NONE,
behavior: "OnOff"
},
time: {
activation: "18:00",
deactivation: "9:00"
},
location: {
latitude: null,
longitude: null
},
previewNewDesign: false,
previewNewestDesign: false,
enableForPDF: true,
enableForProtectedPages: false,
enableContextMenus: false,
detectDarkTheme: true
};
function debounce(delay, fn) {
let timeoutId = null;
return (...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
timeoutId = null;
fn(...args);
}, delay);
};
}
function canInjectScript(url) {
if (url === "about:blank") {
return false;
}
if (isEdge) {
return Boolean(
url &&
!url.startsWith("chrome") &&
!url.startsWith("data") &&
!url.startsWith("devtools") &&
!url.startsWith("edge") &&
!url.startsWith("https://chrome.google.com/webstore") &&
!url.startsWith("https://chromewebstore.google.com/") &&
!url.startsWith(
"https://microsoftedge.microsoft.com/addons"
) &&
!url.startsWith("view-source")
);
}
return Boolean(
url &&
!url.startsWith("chrome") &&
!url.startsWith("https://chrome.google.com/webstore") &&
!url.startsWith("https://chromewebstore.google.com/") &&
!url.startsWith("data") &&
!url.startsWith("devtools") &&
!url.startsWith("view-source")
);
}
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(null);
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(
`sync[${key}]: Could not parse record from sync storage: ${string}`
);
resolve(null);
return;
}
}
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);
chrome.storage.sync.set(packaged, () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve();
});
});
}
async function writeLocalStorage(values) {
return new Promise(async (resolve) => {
chrome.storage.local.set(values, () => {
resolve();
});
});
}
async function removeSyncStorage(keys) {
return new Promise(async (resolve) => {
chrome.storage.sync.remove(keys, () => {
resolve();
});
});
}
async function removeLocalStorage(keys) {
return new Promise(async (resolve) => {
chrome.storage.local.remove(keys, () => {
resolve();
});
});
}
async function getCommands() {
return new Promise((resolve) => {
if (!chrome.commands) {
resolve([]);
return;
}
chrome.commands.getAll((commands) => {
if (commands) {
resolve(commands);
} else {
resolve([]);
}
});
});
}
function keepListeningToEvents() {
let intervalId = 0;
const keepHopeAlive = () => {
intervalId = setInterval(
chrome.runtime.getPlatformInfo,
getDuration({seconds: 10})
);
};
chrome.runtime.onStartup.addListener(keepHopeAlive);
keepHopeAlive();
const stopListening = () => {
clearInterval(intervalId);
chrome.runtime.onStartup.removeListener(keepHopeAlive);
};
return stopListening;
}
class PromiseBarrier {
resolves = [];
rejects = [];
wasResolved = false;
wasRejected = false;
resolution;
reason;
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 = [];
this.rejects = [];
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 = [];
this.rejects = [];
return new Promise((resolve) => setTimeout(() => resolve()));
}
isPending() {
return !this.wasResolved && !this.wasRejected;
}
isFulfilled() {
return this.wasResolved;
}
isRejected() {
return this.wasRejected;
}
}
function isBoolean(x) {
return typeof x === "boolean";
}
function isPlainObject(x) {
return typeof x === "object" && x != null && !Array.isArray(x);
}
function isArray(x) {
return Array.isArray(x);
}
function isString(x) {
return typeof x === "string";
}
function isNonEmptyString(x) {
return x && isString(x);
}
function isNonEmptyArrayOfNonEmptyStrings(x) {
return (
Array.isArray(x) &&
x.length > 0 &&
x.every((s) => isNonEmptyString(s))
);
}
function isRegExpMatch(regexp) {
return (x) => {
return isString(x) && x.match(regexp) != null;
};
}
const isTime = isRegExpMatch(
/^((0?[0-9])|(1[0-9])|(2[0-3])):([0-5][0-9])$/
);
function isNumber(x) {
return typeof x === "number" && !isNaN(x);
}
function isNumberBetween(min, max) {
return (x) => {
return isNumber(x) && x >= min && x <= max;
};
}
function isOneOf(...values) {
return (x) => values.includes(x);
}
function hasRequiredProperties(obj, keys) {
return keys.every((key) => obj.hasOwnProperty(key));
}
function createValidator() {
const errors = [];
function validateProperty(obj, key, validator, fallback) {
if (!obj.hasOwnProperty(key) || validator(obj[key])) {
return;
}
errors.push(
`Unexpected value for "${key}": ${JSON.stringify(obj[key])}`
);
obj[key] = fallback[key];
}
function validateArray(obj, key, validator) {
if (!obj.hasOwnProperty(key)) {
return;
}
const wrongValues = new Set();
const arr = obj[key];
for (let i = 0; i < arr.length; i++) {
if (!validator(arr[i])) {
wrongValues.add(arr[i]);
arr.splice(i, 1);
i--;
}
}
if (wrongValues.size > 0) {
errors.push(
`Array "${key}" has wrong values: ${Array.from(wrongValues)
.map((v) => JSON.stringify(v))
.join("; ")}`
);
}
}
return {validateProperty, validateArray, errors};
}
function validateSettings(settings) {
if (!isPlainObject(settings)) {
return {
errors: ["Settings are not a plain object"],
settings: DEFAULT_SETTINGS
};
}
const {validateProperty, validateArray, errors} = createValidator();
const isValidPresetTheme = (theme) => {
if (!isPlainObject(theme)) {
return false;
}
const {errors: themeErrors} = validateTheme(theme);
return themeErrors.length === 0;
};
validateProperty(settings, "schemeVersion", isNumber, DEFAULT_SETTINGS);
validateProperty(settings, "enabled", isBoolean, DEFAULT_SETTINGS);
validateProperty(settings, "fetchNews", isBoolean, DEFAULT_SETTINGS);
validateProperty(settings, "theme", isPlainObject, DEFAULT_SETTINGS);
const {errors: themeErrors} = validateTheme(settings.theme);
errors.push(...themeErrors);
validateProperty(settings, "presets", isArray, DEFAULT_SETTINGS);
validateArray(settings, "presets", (preset) => {
const presetValidator = createValidator();
if (
!(
isPlainObject(preset) &&
hasRequiredProperties(preset, [
"id",
"name",
"urls",
"theme"
])
)
) {
return false;
}
presetValidator.validateProperty(
preset,
"id",
isNonEmptyString,
preset
);
presetValidator.validateProperty(
preset,
"name",
isNonEmptyString,
preset
);
presetValidator.validateProperty(
preset,
"urls",
isNonEmptyArrayOfNonEmptyStrings,
preset
);
presetValidator.validateProperty(
preset,
"theme",
isValidPresetTheme,
preset
);
return presetValidator.errors.length === 0;
});
validateProperty(settings, "customThemes", isArray, DEFAULT_SETTINGS);
validateArray(settings, "customThemes", (custom) => {
if (
!(
isPlainObject(custom) &&
hasRequiredProperties(custom, ["url", "theme"])
)
) {
return false;
}
const presetValidator = createValidator();
presetValidator.validateProperty(
custom,
"url",
isNonEmptyArrayOfNonEmptyStrings,
custom
);
presetValidator.validateProperty(
custom,
"theme",
isValidPresetTheme,
custom
);
return presetValidator.errors.length === 0;
});
validateProperty(settings, "enabledFor", isArray, DEFAULT_SETTINGS);
validateArray(settings, "enabledFor", isNonEmptyString);
validateProperty(settings, "disabledFor", isArray, DEFAULT_SETTINGS);
validateArray(settings, "disabledFor", isNonEmptyString);
validateProperty(
settings,
"enabledByDefault",
isBoolean,
DEFAULT_SETTINGS
);
validateProperty(
settings,
"changeBrowserTheme",
isBoolean,
DEFAULT_SETTINGS
);
validateProperty(settings, "syncSettings", isBoolean, DEFAULT_SETTINGS);
validateProperty(
settings,
"syncSitesFixes",
isBoolean,
DEFAULT_SETTINGS
);
validateProperty(
settings,
"automation",
(automation) => {
if (!isPlainObject(automation)) {
return false;
}
const automationValidator = createValidator();
automationValidator.validateProperty(
automation,
"enabled",
isBoolean,
automation
);
automationValidator.validateProperty(
automation,
"mode",
isOneOf(
AutomationMode.SYSTEM,
AutomationMode.TIME,
AutomationMode.LOCATION,
AutomationMode.NONE
),
automation
);
automationValidator.validateProperty(
automation,
"behavior",
isOneOf("OnOff", "Scheme"),
automation
);
return automationValidator.errors.length === 0;
},
DEFAULT_SETTINGS
);
validateProperty(
settings,
AutomationMode.TIME,
(time) => {
if (!isPlainObject(time)) {
return false;
}
const timeValidator = createValidator();
timeValidator.validateProperty(
time,
"activation",
isTime,
time
);
timeValidator.validateProperty(
time,
"deactivation",
isTime,
time
);
return timeValidator.errors.length === 0;
},
DEFAULT_SETTINGS
);
validateProperty(
settings,
AutomationMode.LOCATION,
(location) => {
if (!isPlainObject(location)) {
return false;
}
const locValidator = createValidator();
const isValidLoc = (x) => x === null || isNumber(x);
locValidator.validateProperty(
location,
"latitude",
isValidLoc,
location
);
locValidator.validateProperty(
location,
"longitude",
isValidLoc,
location
);
return locValidator.errors.length === 0;
},
DEFAULT_SETTINGS
);
validateProperty(
settings,
"previewNewDesign",
isBoolean,
DEFAULT_SETTINGS
);
validateProperty(
settings,
"previewNewestDesign",
isBoolean,
DEFAULT_SETTINGS
);
validateProperty(settings, "enableForPDF", isBoolean, DEFAULT_SETTINGS);
validateProperty(
settings,
"enableForProtectedPages",
isBoolean,
DEFAULT_SETTINGS
);
validateProperty(
settings,
"enableContextMenus",
isBoolean,
DEFAULT_SETTINGS
);
validateProperty(
settings,
"detectDarkTheme",
isBoolean,
DEFAULT_SETTINGS
);
return {errors, settings};
}
function validateTheme(theme) {
if (!isPlainObject(theme)) {
return {
errors: ["Theme is not a plain object"],
theme: DEFAULT_THEME
};
}
const {validateProperty, errors} = createValidator();
validateProperty(theme, "mode", isOneOf(0, 1), DEFAULT_THEME);
validateProperty(
theme,
"brightness",
isNumberBetween(0, 200),
DEFAULT_THEME
);
validateProperty(
theme,
"contrast",
isNumberBetween(0, 200),
DEFAULT_THEME
);
validateProperty(
theme,
"grayscale",
isNumberBetween(0, 100),
DEFAULT_THEME
);
validateProperty(
theme,
"sepia",
isNumberBetween(0, 100),
DEFAULT_THEME
);
validateProperty(theme, "useFont", isBoolean, DEFAULT_THEME);
validateProperty(theme, "fontFamily", isNonEmptyString, DEFAULT_THEME);
validateProperty(
theme,
"textStroke",
isNumberBetween(0, 1),
DEFAULT_THEME
);
validateProperty(
theme,
"engine",
isOneOf("dynamicTheme", "staticTheme", "cssFilter", "svgFilter"),
DEFAULT_THEME
);
validateProperty(theme, "stylesheet", isString, DEFAULT_THEME);
validateProperty(
theme,
"darkSchemeBackgroundColor",
isRegExpMatch(/^#[0-9a-f]{6}$/i),
DEFAULT_THEME
);
validateProperty(
theme,
"darkSchemeTextColor",
isRegExpMatch(/^#[0-9a-f]{6}$/i),
DEFAULT_THEME
);
validateProperty(
theme,
"lightSchemeBackgroundColor",
isRegExpMatch(/^#[0-9a-f]{6}$/i),
DEFAULT_THEME
);
validateProperty(
theme,
"lightSchemeTextColor",
isRegExpMatch(/^#[0-9a-f]{6}$/i),
DEFAULT_THEME
);
validateProperty(
theme,
"scrollbarColor",
(x) => x === "" || isRegExpMatch(/^(auto)|(#[0-9a-f]{6})$/i)(x),
DEFAULT_THEME
);
validateProperty(
theme,
"selectionColor",
isRegExpMatch(/^(auto)|(#[0-9a-f]{6})$/i),
DEFAULT_THEME
);
validateProperty(
theme,
"styleSystemControls",
isBoolean,
DEFAULT_THEME
);
validateProperty(
theme,
"lightColorScheme",
isNonEmptyString,
DEFAULT_THEME
);
validateProperty(
theme,
"darkColorScheme",
isNonEmptyString,
DEFAULT_THEME
);
validateProperty(theme, "immediateModify", isBoolean, DEFAULT_THEME);
return {errors, theme};
}
const SAVE_TIMEOUT = 1000;
class UserStorage {
static loadBarrier;
static saveStorageBarrier;
static settings;
static async loadSettings() {
if (!UserStorage.settings) {
UserStorage.settings =
await UserStorage.loadSettingsFromStorage();
}
}
static 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};
});
if (settings.customThemes.length === 0) {
settings.customThemes = DEFAULT_SETTINGS.customThemes;
}
}
static migrateAutomationSettings(settings) {
if (typeof settings.automation === "string") {
const automationMode = settings.automation;
const automationBehavior = settings.automationBehaviour;
if (settings.automation === "") {
settings.automation = {
enabled: false,
mode: automationMode,
behavior: automationBehavior
};
} else {
settings.automation = {
enabled: true,
mode: automationMode,
behavior: automationBehavior
};
}
delete settings.automationBehaviour;
}
}
static migrateSiteListsV2(deprecated) {
const settings = {};
settings.enabledByDefault = !deprecated.applyToListedOnly;
if (settings.enabledByDefault) {
settings.disabledFor = deprecated.siteList ?? [];
settings.enabledFor = deprecated.siteListEnabled ?? [];
} else {
settings.disabledFor = [];
settings.enabledFor = deprecated.siteList ?? [];
}
return settings;
}
static async loadSettingsFromStorage() {
if (UserStorage.loadBarrier) {
return await UserStorage.loadBarrier.entry();
}
UserStorage.loadBarrier = new PromiseBarrier();
let local = await readLocalStorage(DEFAULT_SETTINGS);
if (local.schemeVersion < 2) {
const sync = await readSyncStorage({schemeVersion: 0});
if (!sync || sync.schemeVersion < 2) {
const deprecatedDefaults = {
siteList: [],
siteListEnabled: [],
applyToListedOnly: false
};
const localDeprecated =
await readLocalStorage(deprecatedDefaults);
const localTransformed =
UserStorage.migrateSiteListsV2(localDeprecated);
await writeLocalStorage({
schemeVersion: 2,
...localTransformed
});
await removeLocalStorage(Object.keys(deprecatedDefaults));
const syncDeprecated =
await readSyncStorage(deprecatedDefaults);
const syncTransformed =
UserStorage.migrateSiteListsV2(syncDeprecated);
await writeSyncStorage({
schemeVersion: 2,
...syncTransformed
});
await removeSyncStorage(Object.keys(deprecatedDefaults));
local = await readLocalStorage(DEFAULT_SETTINGS);
}
}
const {errors: localCfgErrors} = validateSettings(local);
localCfgErrors.forEach((err) => logWarn(err));
if (local.syncSettings == null) {
local.syncSettings = DEFAULT_SETTINGS.syncSettings;
}
if (!local.syncSettings) {
UserStorage.migrateAutomationSettings(local);
UserStorage.fillDefaults(local);
UserStorage.loadBarrier.resolve(local);
return local;
}
const $sync = await readSyncStorage(DEFAULT_SETTINGS);
if (!$sync) {
local.syncSettings = false;
UserStorage.set({syncSettings: false});
UserStorage.saveSyncSetting(false);
UserStorage.loadBarrier.resolve(local);
return local;
}
const {errors: syncCfgErrors} = validateSettings($sync);
syncCfgErrors.forEach((err) => logWarn(err));
UserStorage.migrateAutomationSettings($sync);
UserStorage.fillDefaults($sync);
UserStorage.loadBarrier.resolve($sync);
return $sync;
}
static async saveSettings() {
if (!UserStorage.settings) {
return;
}
await UserStorage.saveSettingsIntoStorage();
}
static 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
);
UserStorage.set({syncSettings: false});
}
}
static saveSettingsIntoStorage = debounce(SAVE_TIMEOUT, async () => {
if (UserStorage.saveStorageBarrier) {
await UserStorage.saveStorageBarrier.entry();
return;
}
UserStorage.saveStorageBarrier = new PromiseBarrier();
const settings = UserStorage.settings;
if (settings.syncSettings) {
try {
await writeSyncStorage(settings);
} catch (err) {
logWarn(
"Settings synchronization was disabled due to error:",
chrome.runtime.lastError
);
UserStorage.set({syncSettings: false});
await UserStorage.saveSyncSetting(false);
await writeLocalStorage(settings);
}
} else {
await writeLocalStorage(settings);
}
UserStorage.saveStorageBarrier.resolve();
UserStorage.saveStorageBarrier = null;
});
static set($settings) {
if (!UserStorage.settings) {
return;
}
const filterSiteList = (siteList) => {
if (!Array.isArray(siteList)) {
const list = [];
for (const key in siteList) {
const index = Number(key);
if (!isNaN(index)) {
list[index] = siteList[key];
}
}
siteList = list;
}
return siteList.filter((pattern) => {
let isOK = false;
try {
isURLMatched("https://google.com/", pattern);
isURLMatched("[::1]:1337", pattern);
isOK = true;
} catch (err) {}
return isOK && pattern !== "/";
});
};
const {enabledFor, disabledFor} = $settings;
const updatedSettings = {...UserStorage.settings, ...$settings};
if (enabledFor) {
updatedSettings.enabledFor = filterSiteList(enabledFor);
}
if (disabledFor) {
updatedSettings.disabledFor = filterSiteList(disabledFor);
}
UserStorage.settings = updatedSettings;
}
}
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 NEWS_URL = "https://darkreader.org/blog/posts.json";
const UNINSTALL_URL = "https://darkreader.org/goodluck/";
const HELP_URL = "https://darkreader.org/help";
const CONFIG_URL_BASE =
"https://raw.githubusercontent.com/darkreader/darkreader/main/src/config";
const helpLocales = [
"be",
"cs",
"de",
"en",
"es",
"fr",
"it",
"ja",
"nl",
"pt",
"ru",
"sr",
"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 `${HELP_URL}/${matchLocale}/`;
}
function getBlogPostURL(postId) {
return `${BLOG_URL}${postId}/`;
}
const CONFIG_URLs = {
darkSites: {
remote: `${CONFIG_URL_BASE}/dark-sites.config`,
local: "../config/dark-sites.config"
},
dynamicThemeFixes: {
remote: `${CONFIG_URL_BASE}/dynamic-theme-fixes.config`,
local: "../config/dynamic-theme-fixes.config"
},
inversionFixes: {
remote: `${CONFIG_URL_BASE}/inversion-fixes.config`,
local: "../config/inversion-fixes.config"
},
staticThemes: {
remote: `${CONFIG_URL_BASE}/static-themes.config`,
local: "../config/static-themes.config"
},
colorSchemes: {
remote: `${CONFIG_URL_BASE}/color-schemes.drconf`,
local: "../config/color-schemes.drconf"
},
detectorHints: {
remote: `${CONFIG_URL_BASE}/detector-hints.config`,
local: "../config/detector-hints.config"
}
};
const REMOTE_TIMEOUT_MS = getDuration({seconds: 10});
class ConfigManager {
static DARK_SITES_INDEX;
static DETECTOR_HINTS_INDEX;
static DETECTOR_HINTS_RAW;
static DYNAMIC_THEME_FIXES_INDEX;
static DYNAMIC_THEME_FIXES_RAW;
static INVERSION_FIXES_INDEX;
static INVERSION_FIXES_RAW;
static STATIC_THEMES_INDEX;
static STATIC_THEMES_RAW;
static COLOR_SCHEMES_RAW;
static raw = {
darkSites: null,
detectorHints: null,
dynamicThemeFixes: null,
inversionFixes: null,
staticThemes: null,
colorSchemes: null
};
static overrides = {
darkSites: null,
detectorHints: null,
dynamicThemeFixes: null,
inversionFixes: null,
staticThemes: null
};
static 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;
}
static async loadColorSchemes({local}) {
const $config = await ConfigManager.loadConfig({
name: "Color Schemes",
local,
localURL: CONFIG_URLs.colorSchemes.local,
remoteURL: CONFIG_URLs.colorSchemes.remote
});
ConfigManager.raw.colorSchemes = $config;
ConfigManager.handleColorSchemes();
}
static async loadDarkSites({local}) {
const sites = await ConfigManager.loadConfig({
name: "Dark Sites",
local,
localURL: CONFIG_URLs.darkSites.local,
remoteURL: CONFIG_URLs.darkSites.remote
});
ConfigManager.raw.darkSites = sites;
ConfigManager.handleDarkSites();
}
static async loadDetectorHints({local}) {
const $config = await ConfigManager.loadConfig({
name: "Detector Hints",
local,
localURL: CONFIG_URLs.detectorHints.local,
remoteURL: CONFIG_URLs.detectorHints.remote
});
ConfigManager.raw.detectorHints = $config;
ConfigManager.handleDetectorHints();
}
static async loadDynamicThemeFixes({local}) {
const fixes = await ConfigManager.loadConfig({
name: "Dynamic Theme Fixes",
local,
localURL: CONFIG_URLs.dynamicThemeFixes.local,
remoteURL: CONFIG_URLs.dynamicThemeFixes.remote
});
ConfigManager.raw.dynamicThemeFixes = fixes;
ConfigManager.handleDynamicThemeFixes();
}
static async loadInversionFixes({local}) {
const fixes = await ConfigManager.loadConfig({
name: "Inversion Fixes",
local,
localURL: CONFIG_URLs.inversionFixes.local,
remoteURL: CONFIG_URLs.inversionFixes.remote
});
ConfigManager.raw.inversionFixes = fixes;
ConfigManager.handleInversionFixes();
}
static async loadStaticThemes({local}) {
const themes = await ConfigManager.loadConfig({
name: "Static Themes",
local,
localURL: CONFIG_URLs.staticThemes.local,
remoteURL: CONFIG_URLs.staticThemes.remote
});
ConfigManager.raw.staticThemes = themes;
ConfigManager.handleStaticThemes();
}
static async load(config) {
if (!config) {
await UserStorage.loadSettings();
config = {
local: !UserStorage.settings.syncSitesFixes
};
}
await Promise.all([
ConfigManager.loadColorSchemes(config),
ConfigManager.loadDarkSites(config),
ConfigManager.loadDetectorHints(config),
ConfigManager.loadDynamicThemeFixes(config),
ConfigManager.loadInversionFixes(config),
ConfigManager.loadStaticThemes(config)
]).catch((err) => console.error("Fatality", err));
}
static handleColorSchemes() {
const $config = ConfigManager.raw.colorSchemes;
const {result, error} = parseColorSchemeConfig($config || "");
if (error) {
ConfigManager.COLOR_SCHEMES_RAW = DEFAULT_COLORSCHEME;
return;
}
ConfigManager.COLOR_SCHEMES_RAW = result;
}
static handleDarkSites() {
const $sites = ConfigManager.raw.darkSites;
ConfigManager.DARK_SITES_INDEX = indexSiteListConfig($sites || "");
}
static handleDetectorHints() {
const $hints = ConfigManager.raw.detectorHints || "";
ConfigManager.DETECTOR_HINTS_INDEX = indexSitesFixesConfig($hints);
ConfigManager.DETECTOR_HINTS_RAW = $hints;
}
static handleDynamicThemeFixes() {
const $fixes =
ConfigManager.overrides.dynamicThemeFixes ||
ConfigManager.raw.dynamicThemeFixes ||
"";
ConfigManager.DYNAMIC_THEME_FIXES_INDEX =
indexSitesFixesConfig($fixes);
ConfigManager.DYNAMIC_THEME_FIXES_RAW = $fixes;
}
static handleInversionFixes() {
const $fixes =
ConfigManager.overrides.inversionFixes ||
ConfigManager.raw.inversionFixes ||
"";
ConfigManager.INVERSION_FIXES_INDEX = indexSitesFixesConfig($fixes);
ConfigManager.INVERSION_FIXES_RAW = $fixes;
}
static handleStaticThemes() {
const $themes =
ConfigManager.overrides.staticThemes ||
ConfigManager.raw.staticThemes ||
"";
ConfigManager.STATIC_THEMES_INDEX = indexSitesFixesConfig($themes);
ConfigManager.STATIC_THEMES_RAW = $themes;
}
static isURLInDarkList(url) {
return isURLInSiteList(url, ConfigManager.DARK_SITES_INDEX);
}
}
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]
];
}
};
const excludedSelectors = [
"pre",
"pre *",
"code",
'[aria-hidden="true"]',
'[class*="fa-"]',
".fa",
".fab",
".fad",
".fal",
".far",
".fas",
".fass",
".fasr",
".fat",
".icofont",
'[style*="font-"]',
'[class*="icon"]',
'[class*="Icon"]',
'[class*="symbol"]',
'[class*="Symbol"]',
".glyphicon",
'[class*="material-symbol"]',
'[class*="material-icon"]',
"mu",
'[class*="mu-"]',
".typcn",
'[class*="vjs-"]'
];
function createTextStyle(config) {
const lines = [];
lines.push(`*:not(${excludedSelectors.join(", ")}) {`);
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 createCSSFilterStyleSheet(config, url, isTopFrame, fixes, index) {
const filterValue = getCSSFilterValue(config);
const reverseFilterValue = "invert(100%) hue-rotate(180deg)";
return cssFilterStyleSheetTemplate(
"html",
filterValue,
reverseFilterValue,
config,
url,
isTopFrame,
fixes,
index
);
}
function cssFilterStyleSheetTemplate(
filterRoot,
filterValue,
reverseFilterValue,
config,
url,
isTopFrame,
fixes,
index
) {
const fix = getInversionFixesFor(url, fixes, index);
const lines = [];
lines.push("@media screen {");
if (filterValue && isTopFrame) {
lines.push("");
lines.push("/* Leading rule */");
lines.push(createLeadingRule(filterRoot, 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 (isTopFrame) {
const light = [255, 255, 255];
const bgColor = 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(filterRoot, filterValue) {
return [
`${filterRoot} {`,
` -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 cssCommentsRegex = /\/\*[\s\S]*?\*\//g;
function removeCSSComments(cssText) {
return cssText.replace(cssCommentsRegex, "");
}
function parseCSS(cssText) {
cssText = removeCSSComments(cssText);
cssText = cssText.trim();
if (!cssText) {
return [];
}
const rules = [];
const excludeRanges = getTokenExclusionRanges(cssText);
const bracketRanges = getAllOpenCloseRanges(
cssText,
"{",
"}",
excludeRanges
);
let ruleStart = 0;
bracketRanges.forEach((brackets) => {
const key = cssText.substring(ruleStart, brackets.start).trim();
const content = cssText.substring(
brackets.start + 1,
brackets.end - 1
);
if (key.startsWith("@")) {
const typeEndIndex = key.search(/[\s\(]/);
const rule = {
type:
typeEndIndex < 0 ? key : key.substring(0, typeEndIndex),
query:
typeEndIndex < 0
? ""
: key.substring(typeEndIndex).trim(),
rules: parseCSS(content)
};
rules.push(rule);
} else {
const rule = {
selectors: parseSelectors(key),
declarations: parseDeclarations(content)
};
rules.push(rule);
}
ruleStart = brackets.end;
});
return rules;
}
function getAllOpenCloseRanges(
input,
openToken,
closeToken,
excludeRanges = []
) {
const ranges = [];
let i = 0;
let range;
while (
(range = getOpenCloseRange(
input,
i,
openToken,
closeToken,
excludeRanges
))
) {
ranges.push(range);
i = range.end;
}
return ranges;
}
function getTokenExclusionRanges(cssText) {
const singleQuoteGoesFirst =
cssText.indexOf("'") < cssText.indexOf('"');
const firstQuote = singleQuoteGoesFirst ? "'" : '"';
const secondQuote = singleQuoteGoesFirst ? '"' : "'";
const excludeRanges = getAllOpenCloseRanges(
cssText,
firstQuote,
firstQuote
);
excludeRanges.push(
...getAllOpenCloseRanges(
cssText,
secondQuote,
secondQuote,
excludeRanges
)
);
excludeRanges.push(
...getAllOpenCloseRanges(cssText, "[", "]", excludeRanges)
);
excludeRanges.push(
...getAllOpenCloseRanges(cssText, "(", ")", excludeRanges)
);
return excludeRanges;
}
function parseSelectors(selectorText) {
const excludeRanges = getTokenExclusionRanges(selectorText);
return splitExcluding(selectorText, ",", excludeRanges);
}
function parseDeclarations(cssDeclarationsText) {
const declarations = [];
const excludeRanges = getTokenExclusionRanges(cssDeclarationsText);
splitExcluding(cssDeclarationsText, ";", excludeRanges).forEach(
(part) => {
const colonIndex = part.indexOf(":");
if (colonIndex > 0) {
const importantIndex = part.indexOf("!important");
declarations.push({
property: part.substring(0, colonIndex).trim(),
value: part
.substring(
colonIndex + 1,
importantIndex > 0
? importantIndex
: part.length
)
.trim(),
important: importantIndex > 0
});
}
}
);
return declarations;
}
function isParsedStyleRule(rule) {
return "selectors" in rule;
}
function formatCSS(cssText) {
const parsed = parseCSS(cssText);
return formatParsedCSS(parsed);
}
function formatParsedCSS(parsed) {
const lines = [];
const tab = " ";
function formatRule(rule, indent) {
if (isParsedStyleRule(rule)) {
formatStyleRule(rule, indent);
} else {
formatAtRule(rule, indent);
}
}
function formatAtRule({type, query, rules}, indent) {
lines.push(`${indent}${type} ${query} {`);
rules.forEach((child) => formatRule(child, `${indent}${tab}`));
lines.push(`${indent}}`);
}
function formatStyleRule({selectors, declarations}, indent) {
const lastSelectorIndex = selectors.length - 1;
selectors.forEach((selector, i) => {
lines.push(
`${indent}${selector}${i < lastSelectorIndex ? "," : " {"}`
);
});
const sorted = sortDeclarations(declarations);
sorted.forEach(({property, value, important}) => {
lines.push(
`${indent}${tab}${property}: ${value}${important ? " !important" : ""};`
);
});
lines.push(`${indent}}`);
}
clearEmptyRules(parsed);
parsed.forEach((rule) => formatRule(rule, ""));
return lines.join("\n");
}
function sortDeclarations(declarations) {
const prefixRegex = /^-[a-z]-/;
return [...declarations].sort((a, b) => {
const aProp = a.property;
const bProp = b.property;
const aPrefix = aProp.match(prefixRegex)?.[0] ?? "";
const bPrefix = bProp.match(prefixRegex)?.[0] ?? "";
const aNorm = aPrefix ? aProp.replace(prefixRegex, "") : aProp;
const bNorm = bPrefix ? bProp.replace(prefixRegex, "") : bProp;
if (aNorm === bNorm) {
return aPrefix.localeCompare(bPrefix);
}
return aNorm.localeCompare(bNorm);
});
}
function clearEmptyRules(rules) {
for (let i = rules.length - 1; i >= 0; i--) {
const rule = rules[i];
if (isParsedStyleRule(rule)) {
if (rule.declarations.length === 0) {
rules.splice(i, 1);
}
} else {
clearEmptyRules(rule.rules);
if (rule.rules.length === 0) {
rules.splice(i, 1);
}
}
}
}
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 formatCSS(value);
}
return formatArray(value).trim();
},
shouldIgnoreProp: (prop, value) => {
if (prop === "css") {
return !value;
}
return !(Array.isArray(value) && value.length > 0);
}
});
}
function getDynamicThemeFixesFor(
url,
isTopFrame,
text,
index,
enabledForPDF
) {
const fixes = getSitesFixesFor(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;
}
if (enabledForPDF) {
const fixes_ = [...fixes];
fixes_[0] = {...fixes_[0]};
{
fixes_[0].css +=
'\nembed[type="application/pdf"][src="about:blank"] { filter: invert(100%) contrast(90%); }';
}
if (
["drive.google.com", "mail.google.com"].includes(getDomain(url))
) {
fixes_[0].invert.push(
'div[role="dialog"] div[role="document"]'
);
}
return fixes_;
}
return fixes;
}
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,
isTopFrame,
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(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 {
cache = {};
async get(key) {
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]);
});
});
}
async set(key, value) {
this.cache[key] = value;
return new Promise((resolve) =>
chrome.storage.local.set({[key]: value}, () => {
if (chrome.runtime.lastError) {
console.error(
"Failed to write DevTools data",
chrome.runtime.lastError
);
} else {
resolve();
}
})
);
}
async remove(key) {
this.cache[key] = null;
return new Promise((resolve) =>
chrome.storage.local.remove(key, () => {
if (chrome.runtime.lastError) {
console.error(
"Failed to delete DevTools data",
chrome.runtime.lastError
);
} else {
resolve();
}
})
);
}
async has(key) {
return Boolean(await this.get(key));
}
}
class TempStorage {
map = new Map();
async get(key) {
return this.map.get(key) || null;
}
set(key, value) {
this.map.set(key, value);
}
remove(key) {
this.map.delete(key);
}
async has(key) {
return this.map.has(key);
}
}
class DevTools {
static onChange;
static store;
static init(onChange) {
if (
typeof chrome.storage.local !== "undefined" &&
chrome.storage.local !== null
) {
DevTools.store = new PersistentStorageWrapper();
} else {
DevTools.store = new TempStorage();
}
DevTools.loadConfigOverrides();
DevTools.onChange = onChange;
}
static KEY_DYNAMIC = "dev_dynamic_theme_fixes";
static KEY_FILTER = "dev_inversion_fixes";
static KEY_STATIC = "dev_static_themes";
static async loadConfigOverrides() {
const [dynamicThemeFixes, inversionFixes, staticThemes] =
await Promise.all([
DevTools.getSavedDynamicThemeFixes(),
DevTools.getSavedInversionFixes(),
DevTools.getSavedStaticThemes()
]);
ConfigManager.overrides.dynamicThemeFixes =
dynamicThemeFixes || null;
ConfigManager.overrides.inversionFixes = inversionFixes || null;
ConfigManager.overrides.staticThemes = staticThemes || null;
}
static async getSavedDynamicThemeFixes() {
return DevTools.store.get(DevTools.KEY_DYNAMIC);
}
static saveDynamicThemeFixes(text) {
DevTools.store.set(DevTools.KEY_DYNAMIC, text);
}
static async getDynamicThemeFixesText() {
let rawFixes = await DevTools.getSavedDynamicThemeFixes();
if (!rawFixes) {
await ConfigManager.load();
rawFixes = ConfigManager.DYNAMIC_THEME_FIXES_RAW || "";
}
const fixes = parseDynamicThemeFixes(rawFixes);
return formatDynamicThemeFixes(fixes);
}
static resetDynamicThemeFixes() {
DevTools.store.remove(DevTools.KEY_DYNAMIC);
ConfigManager.overrides.dynamicThemeFixes = null;
ConfigManager.handleDynamicThemeFixes();
DevTools.onChange();
}
static applyDynamicThemeFixes(text) {
try {
const formatted = formatDynamicThemeFixes(
parseDynamicThemeFixes(text)
);
ConfigManager.overrides.dynamicThemeFixes = formatted;
ConfigManager.handleDynamicThemeFixes();
DevTools.saveDynamicThemeFixes(formatted);
DevTools.onChange();
return null;
} catch (err) {
return err;
}
}
static async getSavedInversionFixes() {
return this.store.get(DevTools.KEY_FILTER);
}
static saveInversionFixes(text) {
this.store.set(DevTools.KEY_FILTER, text);
}
static async getInversionFixesText() {
let rawFixes = await DevTools.getSavedInversionFixes();
if (!rawFixes) {
await ConfigManager.load();
rawFixes = ConfigManager.INVERSION_FIXES_RAW || "";
}
const fixes = parseInversionFixes(rawFixes);
return formatInversionFixes(fixes);
}
static resetInversionFixes() {
DevTools.store.remove(DevTools.KEY_FILTER);
ConfigManager.overrides.inversionFixes = null;
ConfigManager.handleInversionFixes();
DevTools.onChange();
}
static applyInversionFixes(text) {
try {
const formatted = formatInversionFixes(
parseInversionFixes(text)
);
ConfigManager.overrides.inversionFixes = formatted;
ConfigManager.handleInversionFixes();
DevTools.saveInversionFixes(formatted);
DevTools.onChange();
return null;
} catch (err) {
return err;
}
}
static async getSavedStaticThemes() {
return DevTools.store.get(DevTools.KEY_STATIC);
}
static saveStaticThemes(text) {
DevTools.store.set(DevTools.KEY_STATIC, text);
}
static async getStaticThemesText() {
let rawThemes = await DevTools.getSavedStaticThemes();
if (!rawThemes) {
await ConfigManager.load();
rawThemes = ConfigManager.STATIC_THEMES_RAW || "";
}
const themes = parseStaticThemes(rawThemes);
return formatStaticThemes(themes);
}
static resetStaticThemes() {
DevTools.store.remove(DevTools.KEY_STATIC);
ConfigManager.overrides.staticThemes = null;
ConfigManager.handleStaticThemes();
DevTools.onChange();
}
static applyStaticThemes(text) {
try {
const formatted = formatStaticThemes(parseStaticThemes(text));
ConfigManager.overrides.staticThemes = formatted;
ConfigManager.handleStaticThemes();
DevTools.saveStaticThemes(formatted);
DevTools.onChange();
return null;
} catch (err) {
return err;
}
}
}
class IconManager {
static ICON_PATHS = {
active: {
19: "../icons/dr_active_19.png",
38: "../icons/dr_active_38.png"
},
inactive: {
19: "../icons/dr_inactive_19.png",
38: "../icons/dr_inactive_38.png"
}
};
static iconState = {
badgeText: "",
active: true
};
static onStartup() {}
static handleUpdate() {
if (
IconManager.iconState.badgeText !== "" ||
!IconManager.iconState.active
) {
chrome.runtime.onStartup.addListener(IconManager.onStartup);
} else {
chrome.runtime.onStartup.removeListener(IconManager.onStartup);
}
}
static setActive() {
if (!chrome.action.setIcon) {
return;
}
IconManager.iconState.active = true;
chrome.action.setIcon({
path: IconManager.ICON_PATHS.active
});
IconManager.handleUpdate();
}
static setInactive() {
if (!chrome.action.setIcon) {
return;
}
IconManager.iconState.active = false;
chrome.action.setIcon({
path: IconManager.ICON_PATHS.inactive
});
IconManager.handleUpdate();
}
static showBadge(text) {
IconManager.iconState.badgeText = text;
chrome.action.setBadgeBackgroundColor({color: "#e96c4c"});
chrome.action.setBadgeText({text});
IconManager.handleUpdate();
}
static hideBadge() {
IconManager.iconState.badgeText = "";
chrome.action.setBadgeText({text: ""});
IconManager.handleUpdate();
}
}
var MessageTypeUItoBG;
(function (MessageTypeUItoBG) {
MessageTypeUItoBG["GET_DATA"] = "ui-bg-get-data";
MessageTypeUItoBG["GET_DEVTOOLS_DATA"] = "ui-bg-get-devtools-data";
MessageTypeUItoBG["SUBSCRIBE_TO_CHANGES"] =
"ui-bg-subscribe-to-changes";
MessageTypeUItoBG["UNSUBSCRIBE_FROM_CHANGES"] =
"ui-bg-unsubscribe-from-changes";
MessageTypeUItoBG["CHANGE_SETTINGS"] = "ui-bg-change-settings";
MessageTypeUItoBG["SET_THEME"] = "ui-bg-set-theme";
MessageTypeUItoBG["TOGGLE_ACTIVE_TAB"] = "ui-bg-toggle-active-tab";
MessageTypeUItoBG["MARK_NEWS_AS_READ"] = "ui-bg-mark-news-as-read";
MessageTypeUItoBG["MARK_NEWS_AS_DISPLAYED"] =
"ui-bg-mark-news-as-displayed";
MessageTypeUItoBG["LOAD_CONFIG"] = "ui-bg-load-config";
MessageTypeUItoBG["APPLY_DEV_DYNAMIC_THEME_FIXES"] =
"ui-bg-apply-dev-dynamic-theme-fixes";
MessageTypeUItoBG["RESET_DEV_DYNAMIC_THEME_FIXES"] =
"ui-bg-reset-dev-dynamic-theme-fixes";
MessageTypeUItoBG["APPLY_DEV_INVERSION_FIXES"] =
"ui-bg-apply-dev-inversion-fixes";
MessageTypeUItoBG["RESET_DEV_INVERSION_FIXES"] =
"ui-bg-reset-dev-inversion-fixes";
MessageTypeUItoBG["APPLY_DEV_STATIC_THEMES"] =
"ui-bg-apply-dev-static-themes";
MessageTypeUItoBG["RESET_DEV_STATIC_THEMES"] =
"ui-bg-reset-dev-static-themes";
MessageTypeUItoBG["COLOR_SCHEME_CHANGE"] = "ui-bg-color-scheme-change";
MessageTypeUItoBG["HIDE_HIGHLIGHTS"] = "ui-bg-hide-highlights";
})(MessageTypeUItoBG || (MessageTypeUItoBG = {}));
var MessageTypeBGtoUI;
(function (MessageTypeBGtoUI) {
MessageTypeBGtoUI["CHANGES"] = "bg-ui-changes";
})(MessageTypeBGtoUI || (MessageTypeBGtoUI = {}));
var DebugMessageTypeBGtoUI;
(function (DebugMessageTypeBGtoUI) {
DebugMessageTypeBGtoUI["CSS_UPDATE"] = "debug-bg-ui-css-update";
DebugMessageTypeBGtoUI["UPDATE"] = "debug-bg-ui-update";
})(DebugMessageTypeBGtoUI || (DebugMessageTypeBGtoUI = {}));
var MessageTypeBGtoCS;
(function (MessageTypeBGtoCS) {
MessageTypeBGtoCS["ADD_CSS_FILTER"] = "bg-cs-add-css-filter";
MessageTypeBGtoCS["ADD_DYNAMIC_THEME"] = "bg-cs-add-dynamic-theme";
MessageTypeBGtoCS["ADD_STATIC_THEME"] = "bg-cs-add-static-theme";
MessageTypeBGtoCS["ADD_SVG_FILTER"] = "bg-cs-add-svg-filter";
MessageTypeBGtoCS["CLEAN_UP"] = "bg-cs-clean-up";
MessageTypeBGtoCS["FETCH_RESPONSE"] = "bg-cs-fetch-response";
MessageTypeBGtoCS["UNSUPPORTED_SENDER"] = "bg-cs-unsupported-sender";
})(MessageTypeBGtoCS || (MessageTypeBGtoCS = {}));
var DebugMessageTypeBGtoCS;
(function (DebugMessageTypeBGtoCS) {
DebugMessageTypeBGtoCS["RELOAD"] = "debug-bg-cs-reload";
})(DebugMessageTypeBGtoCS || (DebugMessageTypeBGtoCS = {}));
var MessageTypeCStoBG;
(function (MessageTypeCStoBG) {
MessageTypeCStoBG["COLOR_SCHEME_CHANGE"] = "cs-bg-color-scheme-change";
MessageTypeCStoBG["DARK_THEME_DETECTED"] = "cs-bg-dark-theme-detected";
MessageTypeCStoBG["DARK_THEME_NOT_DETECTED"] =
"cs-bg-dark-theme-not-detected";
MessageTypeCStoBG["FETCH"] = "cs-bg-fetch";
MessageTypeCStoBG["DOCUMENT_CONNECT"] = "cs-bg-document-connect";
MessageTypeCStoBG["DOCUMENT_FORGET"] = "cs-bg-document-forget";
MessageTypeCStoBG["DOCUMENT_FREEZE"] = "cs-bg-document-freeze";
MessageTypeCStoBG["DOCUMENT_RESUME"] = "cs-bg-document-resume";
})(MessageTypeCStoBG || (MessageTypeCStoBG = {}));
var DebugMessageTypeCStoBG;
(function (DebugMessageTypeCStoBG) {
DebugMessageTypeCStoBG["LOG"] = "debug-cs-bg-log";
})(DebugMessageTypeCStoBG || (DebugMessageTypeCStoBG = {}));
var MessageTypeCStoUI;
(function (MessageTypeCStoUI) {
MessageTypeCStoUI["EXPORT_CSS_RESPONSE"] = "cs-ui-export-css-response";
})(MessageTypeCStoUI || (MessageTypeCStoUI = {}));
var MessageTypeUItoCS;
(function (MessageTypeUItoCS) {
MessageTypeUItoCS["EXPORT_CSS"] = "ui-cs-export-css";
})(MessageTypeUItoCS || (MessageTypeUItoCS = {}));
class Messenger {
static adapter;
static changeListenerCount;
static init(adapter) {
Messenger.adapter = adapter;
Messenger.changeListenerCount = 0;
chrome.runtime.onMessage.addListener(Messenger.messageListener);
}
static messageListener(message, sender, sendResponse) {
const allowedSenderURL = [
chrome.runtime.getURL("/ui/popup/index.html"),
chrome.runtime.getURL("/ui/devtools/index.html"),
chrome.runtime.getURL("/ui/options/index.html"),
chrome.runtime.getURL("/ui/stylesheet-editor/index.html")
];
if (allowedSenderURL.includes(sender.url)) {
Messenger.onUIMessage(message, sendResponse);
return [
MessageTypeUItoBG.GET_DATA,
MessageTypeUItoBG.GET_DEVTOOLS_DATA
].includes(message.type);
}
}
static firefoxPortListener(port) {
{
return;
}
}
static onUIMessage({type, data}, sendResponse) {
switch (type) {
case MessageTypeUItoBG.GET_DATA:
Messenger.adapter
.collect()
.then((data) => sendResponse({data}));
break;
case MessageTypeUItoBG.GET_DEVTOOLS_DATA:
Messenger.adapter
.collectDevToolsData()
.then((data) => sendResponse({data}));
break;
case MessageTypeUItoBG.SUBSCRIBE_TO_CHANGES:
Messenger.changeListenerCount++;
break;
case MessageTypeUItoBG.UNSUBSCRIBE_FROM_CHANGES:
Messenger.changeListenerCount--;
break;
case MessageTypeUItoBG.CHANGE_SETTINGS:
Messenger.adapter.changeSettings(data);
break;
case MessageTypeUItoBG.SET_THEME:
Messenger.adapter.setTheme(data);
break;
case MessageTypeUItoBG.TOGGLE_ACTIVE_TAB:
Messenger.adapter.toggleActiveTab();
break;
case MessageTypeUItoBG.MARK_NEWS_AS_READ:
Messenger.adapter.markNewsAsRead(data);
break;
case MessageTypeUItoBG.MARK_NEWS_AS_DISPLAYED:
Messenger.adapter.markNewsAsDisplayed(data);
break;
case MessageTypeUItoBG.LOAD_CONFIG:
Messenger.adapter.loadConfig(data);
break;
case MessageTypeUItoBG.APPLY_DEV_DYNAMIC_THEME_FIXES: {
const error =
Messenger.adapter.applyDevDynamicThemeFixes(data);
sendResponse({error: error ? error.message : undefined});
break;
}
case MessageTypeUItoBG.RESET_DEV_DYNAMIC_THEME_FIXES:
Messenger.adapter.resetDevDynamicThemeFixes();
break;
case MessageTypeUItoBG.APPLY_DEV_INVERSION_FIXES: {
const error =
Messenger.adapter.applyDevInversionFixes(data);
sendResponse({error: error ? error.message : undefined});
break;
}
case MessageTypeUItoBG.RESET_DEV_INVERSION_FIXES:
Messenger.adapter.resetDevInversionFixes();
break;
case MessageTypeUItoBG.APPLY_DEV_STATIC_THEMES: {
const error = Messenger.adapter.applyDevStaticThemes(data);
sendResponse({error: error ? error.message : undefined});
break;
}
case MessageTypeUItoBG.RESET_DEV_STATIC_THEMES:
Messenger.adapter.resetDevStaticThemes();
break;
case MessageTypeUItoBG.HIDE_HIGHLIGHTS:
Messenger.adapter.hideHighlights(data);
break;
}
}
static reportChanges(data) {
if (Messenger.changeListenerCount > 0) {
chrome.runtime.sendMessage({
type: MessageTypeBGtoUI.CHANGES,
data
});
}
}
}
var StateManagerImplState;
(function (StateManagerImplState) {
StateManagerImplState[(StateManagerImplState["INITIAL"] = 0)] =
"INITIAL";
StateManagerImplState[(StateManagerImplState["LOADING"] = 1)] =
"LOADING";
StateManagerImplState[(StateManagerImplState["READY"] = 2)] = "READY";
StateManagerImplState[(StateManagerImplState["SAVING"] = 3)] = "SAVING";
StateManagerImplState[(StateManagerImplState["SAVING_OVERRIDE"] = 4)] =
"SAVING_OVERRIDE";
StateManagerImplState[(StateManagerImplState["ONCHANGE_RACE"] = 5)] =
"ONCHANGE_RACE";
StateManagerImplState[(StateManagerImplState["RECOVERY"] = 6)] =
"RECOVERY";
})(StateManagerImplState || (StateManagerImplState = {}));
class StateManagerImpl {
localStorageKey;
parent;
defaults;
logWarn;
meta;
barrier = null;
storage;
listeners;
constructor(
localStorageKey,
parent,
defaults,
storage,
addListener,
logWarn
) {
this.localStorageKey = localStorageKey;
this.parent = parent;
this.defaults = defaults;
this.storage = storage;
addListener((change) => this.onChange(change));
this.logWarn = logWarn;
this.meta = StateManagerImplState.INITIAL;
this.barrier = new PromiseBarrier();
this.listeners = new Set();
}
collectState() {
const state = {};
for (const key of Object.keys(this.defaults)) {
state[key] = this.parent[key] || this.defaults[key];
}
return state;
}
applyState(storage) {
Object.assign(this.parent, this.defaults, storage);
}
releaseBarrier() {
const barrier = this.barrier;
this.barrier = new PromiseBarrier();
barrier.resolve();
}
notifyListeners() {
this.listeners.forEach((listener) => listener());
}
onChange(state) {
switch (this.meta) {
case StateManagerImplState.INITIAL:
this.meta = StateManagerImplState.READY;
case StateManagerImplState.READY:
this.applyState(state);
this.notifyListeners();
return;
case StateManagerImplState.LOADING:
this.meta = StateManagerImplState.ONCHANGE_RACE;
return;
case StateManagerImplState.SAVING:
this.meta = StateManagerImplState.ONCHANGE_RACE;
return;
case StateManagerImplState.SAVING_OVERRIDE:
this.meta = StateManagerImplState.ONCHANGE_RACE;
break;
case StateManagerImplState.ONCHANGE_RACE:
break;
case StateManagerImplState.RECOVERY:
this.meta = StateManagerImplState.ONCHANGE_RACE;
break;
}
}
saveStateInternal() {
this.storage.set(
{[this.localStorageKey]: this.collectState()},
() => {
switch (this.meta) {
case StateManagerImplState.INITIAL:
case StateManagerImplState.LOADING:
case StateManagerImplState.READY:
case StateManagerImplState.RECOVERY:
this.logWarn(
"Unexpected state. Possible data race!"
);
this.meta = StateManagerImplState.ONCHANGE_RACE;
this.loadStateInternal();
return;
case StateManagerImplState.SAVING:
this.meta = StateManagerImplState.READY;
this.releaseBarrier();
return;
case StateManagerImplState.SAVING_OVERRIDE:
this.meta = StateManagerImplState.SAVING;
this.saveStateInternal();
return;
case StateManagerImplState.ONCHANGE_RACE:
this.meta = StateManagerImplState.RECOVERY;
this.loadStateInternal();
}
}
);
}
async saveState() {
switch (this.meta) {
case StateManagerImplState.INITIAL:
this.logWarn(
"StateManager.saveState was called before StateManager.loadState(). Possible data race! Loading data instead."
);
return this.loadState();
case StateManagerImplState.LOADING:
this.logWarn(
"StateManager.saveState was called before StateManager.loadState() resolved. Possible data race! Loading data instead."
);
return this.barrier.entry();
case StateManagerImplState.READY:
this.meta = StateManagerImplState.SAVING;
const entry = this.barrier.entry();
this.saveStateInternal();
return entry;
case StateManagerImplState.SAVING:
this.meta = StateManagerImplState.SAVING_OVERRIDE;
return this.barrier.entry();
case StateManagerImplState.SAVING_OVERRIDE:
return this.barrier.entry();
case StateManagerImplState.ONCHANGE_RACE:
this.logWarn(
"StateManager.saveState was called during active read/write operation. Possible data race! Loading data instead."
);
return this.barrier.entry();
case StateManagerImplState.RECOVERY:
this.logWarn(
"StateManager.saveState was called during active read operation. Possible data race! Waiting for data load instead."
);
return this.barrier.entry();
}
}
loadStateInternal() {
this.storage.get(this.localStorageKey, (data) => {
switch (this.meta) {
case StateManagerImplState.INITIAL:
case StateManagerImplState.READY:
case StateManagerImplState.SAVING:
case StateManagerImplState.SAVING_OVERRIDE:
this.logWarn("Unexpected state. Possible data race!");
return;
case StateManagerImplState.LOADING:
this.meta = StateManagerImplState.READY;
this.applyState(data[this.localStorageKey]);
this.releaseBarrier();
return;
case StateManagerImplState.ONCHANGE_RACE:
this.meta = StateManagerImplState.RECOVERY;
this.loadStateInternal();
case StateManagerImplState.RECOVERY:
this.meta = StateManagerImplState.READY;
this.applyState(data[this.localStorageKey]);
this.releaseBarrier();
this.notifyListeners();
}
});
}
async loadState() {
switch (this.meta) {
case StateManagerImplState.INITIAL:
this.meta = StateManagerImplState.LOADING;
const entry = this.barrier.entry();
this.loadStateInternal();
return entry;
case StateManagerImplState.READY:
return;
case StateManagerImplState.SAVING:
return this.barrier.entry();
case StateManagerImplState.SAVING_OVERRIDE:
return this.barrier.entry();
case StateManagerImplState.LOADING:
return this.barrier.entry();
case StateManagerImplState.ONCHANGE_RACE:
return this.barrier.entry();
case StateManagerImplState.RECOVERY:
return this.barrier.entry();
}
}
addChangeListener(callback) {
this.listeners.add(callback);
}
getStateForTesting() {
{
return "";
}
}
}
class StateManager {
stateManager;
constructor(localStorageKey, parent, defaults, logWarn) {
{
function addListener(listener) {
chrome.storage.local.onChanged.addListener((changes) => {
if (localStorageKey in changes) {
listener(changes[localStorageKey].newValue);
}
});
}
this.stateManager = new StateManagerImpl(
localStorageKey,
parent,
defaults,
chrome.storage.local,
addListener,
logWarn
);
}
}
async saveState() {
if (this.stateManager) {
return this.stateManager.saveState();
}
}
async loadState() {
if (this.stateManager) {
return this.stateManager.loadState();
}
}
}
class Newsmaker {
static UPDATE_INTERVAL = getDurationInMinutes({hours: 4});
static ALARM_NAME = "newsmaker";
static LOCAL_STORAGE_KEY = "Newsmaker-state";
static initialized;
static stateManager;
static latest;
static latestTimestamp;
static init() {
if (Newsmaker.initialized) {
return;
}
Newsmaker.initialized = true;
Newsmaker.stateManager = new StateManager(
Newsmaker.LOCAL_STORAGE_KEY,
this,
{latest: [], latestTimestamp: null},
logWarn
);
Newsmaker.latest = [];
Newsmaker.latestTimestamp = null;
}
static onUpdate() {
Newsmaker.init();
const latestNews =
Newsmaker.latest.length > 0 && Newsmaker.latest[0];
if (
latestNews &&
latestNews.badge &&
!latestNews.read &&
!latestNews.displayed
) {
IconManager.showBadge(latestNews.badge);
return;
}
IconManager.hideBadge();
}
static async getLatest() {
Newsmaker.init();
await Newsmaker.stateManager.loadState();
return Newsmaker.latest;
}
static alarmListener = (alarm) => {
Newsmaker.init();
if (alarm.name === Newsmaker.ALARM_NAME) {
Newsmaker.updateNews();
}
};
static subscribe() {
Newsmaker.init();
if (
Newsmaker.latestTimestamp === null ||
Newsmaker.latestTimestamp + Newsmaker.UPDATE_INTERVAL <
Date.now()
) {
Newsmaker.updateNews();
}
chrome.alarms.onAlarm.addListener(Newsmaker.alarmListener);
chrome.alarms.create(Newsmaker.ALARM_NAME, {
periodInMinutes: Newsmaker.UPDATE_INTERVAL
});
}
static unSubscribe() {
chrome.alarms.onAlarm.removeListener(Newsmaker.alarmListener);
chrome.alarms.clear(Newsmaker.ALARM_NAME);
}
static async updateNews() {
Newsmaker.init();
const news = await Newsmaker.getNews();
if (Array.isArray(news)) {
Newsmaker.latest = news;
Newsmaker.latestTimestamp = Date.now();
Newsmaker.onUpdate();
await Newsmaker.stateManager.saveState();
}
}
static async getReadNews() {
Newsmaker.init();
const [sync, local] = await Promise.all([
readSyncStorage({readNews: []}),
readLocalStorage({readNews: []})
]);
return Array.from(
new Set([
...(sync ? sync.readNews : []),
...(local ? local.readNews : [])
])
);
}
static async getDisplayedNews() {
Newsmaker.init();
const [sync, local] = await Promise.all([
readSyncStorage({displayedNews: []}),
readLocalStorage({displayedNews: []})
]);
return Array.from(
new Set([
...(sync ? sync.displayedNews : []),
...(local ? local.displayedNews : [])
])
);
}
static async getNews() {
Newsmaker.init();
try {
const response = await fetch(NEWS_URL, {cache: "no-cache"});
const $news = await response.json();
const readNews = await Newsmaker.getReadNews();
const displayedNews = await Newsmaker.getDisplayedNews();
const news = $news.map((n) => {
const url = getBlogPostURL(n.id);
const read = Newsmaker.wasRead(n.id, readNews);
const displayed = Newsmaker.wasDisplayed(
n.id,
displayedNews
);
return {...n, url, read, displayed};
});
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;
}
}
static async markAsRead(ids) {
Newsmaker.init();
const readNews = await Newsmaker.getReadNews();
const results = readNews.slice();
let changed = false;
ids.forEach((id) => {
if (readNews.indexOf(id) < 0) {
results.push(id);
changed = true;
}
});
if (changed) {
Newsmaker.latest = Newsmaker.latest.map((n) => {
const read = Newsmaker.wasRead(n.id, results);
return {...n, read};
});
Newsmaker.onUpdate();
const obj = {readNews: results};
await Promise.all([
writeLocalStorage(obj),
writeSyncStorage(obj),
Newsmaker.stateManager.saveState()
]);
}
}
static async markAsDisplayed(ids) {
Newsmaker.init();
const displayedNews = await Newsmaker.getDisplayedNews();
const results = displayedNews.slice();
let changed = false;
ids.forEach((id) => {
if (displayedNews.indexOf(id) < 0) {
results.push(id);
changed = true;
}
});
if (changed) {
Newsmaker.latest = Newsmaker.latest.map((n) => {
const displayed = Newsmaker.wasDisplayed(n.id, results);
return {...n, displayed};
});
Newsmaker.onUpdate();
const obj = {displayedNews: results};
await Promise.all([
writeLocalStorage(obj),
writeSyncStorage(obj),
Newsmaker.stateManager.saveState()
]);
}
}
static wasRead(id, readNews) {
return readNews.includes(id);
}
static wasDisplayed(id, displayedNews) {
return displayedNews.includes(id);
}
}
function isPanel(sender) {
return (
typeof sender === "undefined" ||
typeof sender.tab === "undefined" ||
(isOpera && sender.tab.index === -1)
);
}
async function queryTabs(query = {}) {
return new Promise((resolve) => chrome.tabs.query(query, resolve));
}
async function getActiveTab() {
let tab = (
await queryTabs({
active: true,
lastFocusedWindow: true,
windowType: "normal"
})
)[0];
if (!tab) {
tab = (
await queryTabs({
active: true,
lastFocusedWindow: true,
windowType: "app"
})
)[0];
}
if (!tab) {
tab = (
await queryTabs({
active: true,
windowType: "normal"
})
)[0];
}
if (!tab) {
tab = (
await queryTabs({
active: true,
windowType: "app"
})
)[0];
}
return tab || null;
}
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 {
static tabs;
static stateManager;
static fileLoader = null;
static onColorSchemeChange;
static getTabMessage;
static timestamp;
static LOCAL_STORAGE_KEY = "TabManager-state";
static init({
getConnectionMessage,
onColorSchemeChange,
getTabMessage
}) {
TabManager.stateManager = new StateManager(
TabManager.LOCAL_STORAGE_KEY,
this,
{tabs: {}, timestamp: 0},
logWarn
);
TabManager.tabs = {};
TabManager.onColorSchemeChange = onColorSchemeChange;
TabManager.getTabMessage = getTabMessage;
chrome.runtime.onMessage.addListener(
async (message, sender, sendResponse) => {
switch (message.type) {
case MessageTypeCStoBG.DOCUMENT_CONNECT: {
if (isPanel(sender)) {
sendResponse({
type: MessageTypeBGtoCS.UNSUPPORTED_SENDER
});
return;
}
TabManager.onColorSchemeMessage(message, sender);
await TabManager.stateManager.loadState();
const reply = (
tabURL,
url,
isTopFrame,
topFrameHasDarkTheme
) => {
getConnectionMessage(
tabURL,
url,
isTopFrame,
topFrameHasDarkTheme
).then((response) => {
if (!response) {
return;
}
response.scriptId = message.scriptId;
TabManager.sendDocumentMessage(
sender.tab.id,
sender.documentId,
response,
sender.frameId
);
});
};
if (isPanel(sender)) {
{
sendResponse("unsupportedSender");
}
return;
}
const {frameId} = sender;
const isTopFrame =
frameId === 0 || message.data.isTopFrame;
const url = sender.url;
const tabId = sender.tab.id;
const scriptId = message.scriptId;
const topFrameHasDarkTheme = isTopFrame
? false
: TabManager.tabs[tabId]?.[0]
?.darkThemeDetected;
const tabURL = isTopFrame ? url : sender.tab.url;
const documentId = sender.documentId;
TabManager.addFrame(
tabId,
frameId,
documentId,
scriptId,
url,
isTopFrame
);
reply(
tabURL,
url,
isTopFrame,
topFrameHasDarkTheme
);
TabManager.stateManager.saveState();
break;
}
case MessageTypeCStoBG.DOCUMENT_FORGET:
if (!sender.tab) {
break;
}
TabManager.removeFrame(
sender.tab.id,
sender.frameId
);
break;
case MessageTypeCStoBG.DOCUMENT_FREEZE: {
await TabManager.stateManager.loadState();
const info =
TabManager.tabs[sender.tab.id][sender.frameId];
info.state = DocumentState.FROZEN;
info.url = null;
TabManager.stateManager.saveState();
break;
}
case MessageTypeCStoBG.DOCUMENT_RESUME: {
TabManager.onColorSchemeMessage(message, sender);
await TabManager.stateManager.loadState();
const tabId = sender.tab.id;
const tabURL = sender.tab.url;
const frameId = sender.frameId;
const url = sender.url;
const documentId = sender.documentId;
const isTopFrame =
frameId === 0 || message.data.isTopFrame;
if (
TabManager.tabs[tabId][frameId].timestamp <
TabManager.timestamp
) {
const response = TabManager.getTabMessage(
tabURL,
url,
isTopFrame
);
response.scriptId = message.scriptId;
TabManager.sendDocumentMessage(
tabId,
documentId,
response,
frameId
);
}
TabManager.tabs[sender.tab.id][sender.frameId] = {
documentId,
scriptId: message.scriptId,
url,
isTop: isTopFrame || undefined,
state: DocumentState.ACTIVE,
darkThemeDetected: false,
timestamp: TabManager.timestamp
};
TabManager.stateManager.saveState();
break;
}
case MessageTypeCStoBG.DARK_THEME_DETECTED:
const tabId = sender.tab.id;
const frames = TabManager.tabs[tabId];
if (!frames) {
break;
}
for (const entry of Object.entries(frames)) {
const frameId = Number(entry[0]);
const frame = entry[1];
frame.darkThemeDetected = true;
const {documentId, scriptId} = frame;
if (
sender.frameId === 0 &&
!frame.isTop &&
frameId &&
documentId
) {
const message = {
type: MessageTypeBGtoCS.CLEAN_UP,
scriptId
};
TabManager.sendDocumentMessage(
tabId,
documentId,
message,
frameId
);
}
}
break;
case MessageTypeCStoBG.FETCH: {
const id = message.id;
const sendResponse = (response) => {
TabManager.sendDocumentMessage(
sender.tab.id,
sender.documentId,
{
type: MessageTypeBGtoCS.FETCH_RESPONSE,
id,
...response
},
sender.frameId
);
};
try {
const {url, responseType, mimeType, origin} =
message.data;
if (!TabManager.fileLoader) {
TabManager.fileLoader = createFileLoader();
}
const response =
await TabManager.fileLoader.get({
url,
responseType,
mimeType,
origin
});
sendResponse({data: response});
} catch (err) {
sendResponse({
error:
err && err.message ? err.message : err
});
}
break;
}
case MessageTypeUItoBG.COLOR_SCHEME_CHANGE:
case MessageTypeCStoBG.COLOR_SCHEME_CHANGE:
TabManager.onColorSchemeMessage(message, sender);
break;
}
}
);
chrome.tabs.onRemoved.addListener(async (tabId) =>
TabManager.removeFrame(tabId, 0)
);
}
static sendDocumentMessage(tabId, documentId, message, frameId) {
{
chrome.tabs
.sendMessage(tabId, message, {documentId})
.catch(() =>
chrome.tabs
.sendMessage(tabId, message, {frameId, documentId})
.catch(() =>
chrome.tabs
.sendMessage(tabId, message, {documentId})
.catch(() => {})
)
);
return;
}
}
static onColorSchemeMessage(message, sender) {
if (sender && sender.frameId === 0) {
TabManager.onColorSchemeChange(message.data.isDark);
}
}
static addFrame(tabId, frameId, documentId, scriptId, url, isTop) {
let frames;
if (TabManager.tabs[tabId]) {
frames = TabManager.tabs[tabId];
} else {
frames = {};
TabManager.tabs[tabId] = frames;
}
frames[frameId] = {
documentId,
scriptId,
url,
isTop: isTop || undefined,
state: DocumentState.ACTIVE,
darkThemeDetected: false,
timestamp: TabManager.timestamp
};
}
static async removeFrame(tabId, frameId) {
await TabManager.stateManager.loadState();
if (frameId === 0) {
delete TabManager.tabs[tabId];
}
if (TabManager.tabs[tabId] && TabManager.tabs[tabId][frameId]) {
delete TabManager.tabs[tabId][frameId];
}
TabManager.stateManager.saveState();
}
static async getTabURL(tab) {
{
if (!tab) {
return "about:blank";
}
try {
return (await chrome.tabs.get(tab.id)).url || "about:blank";
} catch (e) {
try {
return (
(
await chrome.scripting.executeScript({
target: {
tabId: tab.id,
frameIds: [0]
},
world: "MAIN",
injectImmediately: true,
func: () => window.location.href
})
)[0].result || "about:blank"
);
} catch (e) {
const errMessage = String(e);
if (
errMessage.includes("chrome://") ||
errMessage.includes("chrome-extension://") ||
errMessage.includes("gallery")
) {
return "chrome://protected";
}
return "about:blank";
}
}
}
return (tab && tab.url) || "about:blank";
}
static async updateContentScript(options) {
(await queryTabs({discarded: false}))
.filter((tab) => true)
.filter((tab) => !Boolean(TabManager.tabs[tab.id]))
.forEach((tab) => {
{
chrome.scripting.executeScript(
{
target: {
tabId: tab.id,
allFrames: true
},
files: ["/inject/index.js"]
},
() =>
logInfo(
"Could not update content script in tab",
tab,
chrome.runtime.lastError
)
);
}
});
}
static async registerMailDisplayScript() {
await chrome.messageDisplayScripts.register({
js: [{file: "/inject/fallback.js"}, {file: "/inject/index.js"}]
});
}
static async sendMessage(onlyUpdateActiveTab = false) {
TabManager.timestamp++;
const activeTabHostname = onlyUpdateActiveTab
? getURLHostOrProtocol(await TabManager.getActiveTabURL())
: null;
(await queryTabs({discarded: false}))
.filter((tab) => Boolean(TabManager.tabs[tab.id]))
.forEach((tab) => {
const frames = TabManager.tabs[tab.id];
Object.entries(frames)
.filter(
([, {state}]) =>
state === DocumentState.ACTIVE ||
state === DocumentState.PASSIVE
)
.forEach(
async ([
id,
{url, documentId, scriptId, isTop}
]) => {
const frameId = Number(id);
const tabURL = await TabManager.getTabURL(tab);
if (
onlyUpdateActiveTab &&
getURLHostOrProtocol(tabURL) !==
activeTabHostname
) {
return;
}
const message = TabManager.getTabMessage(
tabURL,
url,
isTop || false
);
message.scriptId = scriptId;
if (tab.active && isTop) {
TabManager.sendDocumentMessage(
tab.id,
documentId,
message,
frameId
);
} else {
setTimeout(() => {
TabManager.sendDocumentMessage(
tab.id,
documentId,
message,
frameId
);
});
}
if (TabManager.tabs[tab.id][frameId]) {
TabManager.tabs[tab.id][frameId].timestamp =
TabManager.timestamp;
}
}
);
});
}
static canAccessTab(tab) {
return (tab && Boolean(TabManager.tabs[tab.id])) || false;
}
static getTabDocumentId(tab) {
return (
tab &&
TabManager.tabs[tab.id] &&
TabManager.tabs[tab.id][0] &&
TabManager.tabs[tab.id][0].documentId
);
}
static isTabDarkThemeDetected(tab) {
return (
(tab &&
TabManager.tabs[tab.id] &&
TabManager.tabs[tab.id][0] &&
TabManager.tabs[tab.id][0].darkThemeDetected) ||
null
);
}
static async getActiveTabURL() {
return TabManager.getTabURL(await getActiveTab());
}
}
function evalMath(expression) {
const rpnStack = [];
const workingStack = [];
let lastToken;
for (let i = 0, len = expression.length; i < len; i++) {
const token = expression[i];
if (!token || token === " ") {
continue;
}
if (operators.has(token)) {
const op = operators.get(token);
while (workingStack.length) {
const currentOp = operators.get(workingStack[0]);
if (!currentOp) {
break;
}
if (op.lessOrEqualThan(currentOp)) {
rpnStack.push(workingStack.shift());
} else {
break;
}
}
workingStack.unshift(token);
} else if (!lastToken || operators.has(lastToken)) {
rpnStack.push(token);
} else {
rpnStack[rpnStack.length - 1] += token;
}
lastToken = token;
}
rpnStack.push(...workingStack);
const stack = [];
for (let i = 0, len = rpnStack.length; i < len; i++) {
const op = operators.get(rpnStack[i]);
if (op) {
const args = stack.splice(0, 2);
stack.push(op.exec(args[1], args[0]));
} else {
stack.unshift(parseFloat(rpnStack[i]));
}
}
return stack[0];
}
class Operator {
precendce;
execMethod;
constructor(precedence, method) {
this.precendce = precedence;
this.execMethod = method;
}
exec(left, right) {
return this.execMethod(left, right);
}
lessOrEqualThan(op) {
return this.precendce <= op.precendce;
}
}
const operators = new Map([
["+", new Operator(1, (left, right) => left + right)],
["-", new Operator(1, (left, right) => left - right)],
["*", new Operator(2, (left, right) => left * right)],
["/", new Operator(2, (left, right) => left / right)]
]);
const isSystemDarkModeEnabled = () =>
matchMedia("(prefers-color-scheme: dark)").matches;
const hslaParseCache = new Map();
const rgbaParseCache = new Map();
function parseColorWithCache($color) {
$color = $color.trim();
if (rgbaParseCache.has($color)) {
return rgbaParseCache.get($color);
}
if ($color.includes("calc(")) {
$color = lowerCalcExpression($color);
}
const color = parse($color);
color && rgbaParseCache.set($color, color);
return color;
}
function parseToHSLWithCache(color) {
if (hslaParseCache.has(color)) {
return hslaParseCache.get(color);
}
const rgb = parseColorWithCache(color);
if (!rgb) {
return null;
}
const hsl = rgbToHSL(rgb);
hslaParseCache.set(color, hsl);
return hsl;
}
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};
}
if (
(c.startsWith("color(") || c.startsWith("color-mix(")) &&
c.endsWith(")")
) {
return domParseColor(c);
}
if (c.startsWith("light-dark(") && c.endsWith(")")) {
const match = c.match(
/^light-dark\(\s*([a-z]+(\(.*\))?),\s*([a-z]+(\(.*\))?)\s*\)$/
);
if (match) {
const schemeColor = isSystemDarkModeEnabled()
? match[3]
: match[1];
return parse(schemeColor);
}
}
return null;
}
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 === "," || 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};
}
}
return null;
}
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
};
}
function lowerCalcExpression(color) {
let searchIndex = 0;
const replaceBetweenIndices = (start, end, replacement) => {
color =
color.substring(0, start) + replacement + color.substring(end);
};
while ((searchIndex = color.indexOf("calc(")) !== -1) {
const range = getParenthesesRange(color, searchIndex);
if (!range) {
break;
}
let slice = color.slice(range.start + 1, range.end - 1);
const includesPercentage = slice.includes("%");
slice = slice.split("%").join("");
const output = Math.round(evalMath(slice));
replaceBetweenIndices(
range.start - 4,
range.end,
output + (includesPercentage ? "%" : "")
);
}
return color;
}
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])
);
let canvas;
let context;
function domParseColor($color) {
if (!context) {
canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
context = canvas.getContext("2d", {willReadFrequently: true});
}
context.fillStyle = $color;
context.fillRect(0, 0, 1, 1);
const d = context.getImageData(0, 0, 1, 1).data;
const color = `rgba(${d[0]}, ${d[1]}, ${d[2]}, ${(d[3] / 255).toFixed(2)})`;
return parseRGB(color);
}
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 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};
}
let 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);
}
}
if (hx > 40 && hx < 80) {
lx *= 0.75;
}
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(theme) {
const colors = Object.entries($colors).reduce((obj, [key, value]) => {
const type = themeColorTypes[key];
const modify = {
bg: modifyBackgroundColor,
text: modifyForegroundColor,
border: modifyBorderColor
}[type];
const rgb = parseColorWithCache(value);
const modified = modify(rgb, theme);
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();
}
}
const detectorHintsCommands = {
"TARGET": "target",
"MATCH": "match",
"NO DARK THEME": "noDarkTheme",
"SYSTEM THEME": "systemTheme"
};
const detectorParserOptions = {
commands: Object.keys(detectorHintsCommands),
getCommandPropName: (command) => detectorHintsCommands[command],
parseCommandValue: (command, value) => {
if (command === "TARGET") {
return value.trim();
}
if (command === "NO DARK THEME" || command === "SYSTEM THEME") {
return true;
}
return parseArray(value);
}
};
function getDetectorHintsFor(url, text, index) {
const fixes = getSitesFixesFor(url, text, index, detectorParserOptions);
if (fixes.length === 0) {
return null;
}
return fixes;
}
function createSVGFilterStylesheet(config, url, isTopFrame, fixes, index) {
let filterValue;
let reverseFilterValue;
{
filterValue = "url(#dark-reader-filter)";
reverseFilterValue = "url(#dark-reader-reverse-filter)";
}
const filterRoot = "html";
return cssFilterStyleSheetTemplate(
filterRoot,
filterValue,
reverseFilterValue,
config,
url,
isTopFrame,
fixes,
index
);
}
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 proposedHighlights = ["new-toggle-menus"];
const KEY_UI_HIDDEN_HIGHLIGHTS = "ui-hidden-highlights";
async function getHiddenHighlights() {
const options = await readLocalStorage({
[KEY_UI_HIDDEN_HIGHLIGHTS]: []
});
return options[KEY_UI_HIDDEN_HIGHLIGHTS];
}
async function getHighlightsToShow() {
const hiddenHighlights = await getHiddenHighlights();
return proposedHighlights.filter((h) => !hiddenHighlights.includes(h));
}
async function hideHighlights(keys) {
const hiddenHighlights = await getHiddenHighlights();
const update = Array.from(new Set([...hiddenHighlights, ...keys]));
await writeLocalStorage({[KEY_UI_HIDDEN_HIGHLIGHTS]: update});
}
var UIHighlights = {
getHighlightsToShow,
hideHighlights
};
class Extension {
static autoState = "";
static wasEnabledOnLastCheck = null;
static registeredContextMenus = null;
static wasLastColorSchemeDark = null;
static startBarrier = null;
static stateManager = null;
static ALARM_NAME = "auto-time-alarm";
static LOCAL_STORAGE_KEY = "Extension-state";
static SYSTEM_COLOR_LOCAL_STORAGE_KEY = "system-color-state";
static systemColorStateManager;
static initialized = false;
static isFirstLoad = false;
static init() {
if (Extension.initialized) {
return;
}
Extension.initialized = true;
DevTools.init(Extension.onSettingsChanged);
Messenger.init(Extension.getMessengerAdapter());
TabManager.init({
getConnectionMessage: Extension.getConnectionMessage,
getTabMessage: Extension.getTabMessage,
onColorSchemeChange: Extension.onColorSchemeChange
});
Extension.startBarrier = new PromiseBarrier();
Extension.stateManager = new StateManager(
Extension.LOCAL_STORAGE_KEY,
Extension,
{
autoState: "",
wasEnabledOnLastCheck: null,
registeredContextMenus: null
},
logWarn
);
chrome.alarms.onAlarm.addListener(Extension.alarmListener);
if (chrome.commands) {
{
chrome.commands.onCommand.addListener(
async (command, tab) =>
Extension.onCommand(
command,
(tab && tab.id) || null,
0,
null
)
);
}
}
if (chrome.permissions.onRemoved) {
chrome.permissions.onRemoved.addListener((permissions) => {
if (!permissions?.permissions?.includes("contextMenus")) {
Extension.registeredContextMenus = false;
}
});
}
}
static async MV3syncSystemColorStateManager(isDark) {
if (!Extension.systemColorStateManager) {
Extension.systemColorStateManager = new StateManager(
Extension.SYSTEM_COLOR_LOCAL_STORAGE_KEY,
Extension,
{
wasLastColorSchemeDark: isDark
},
logWarn
);
}
if (isDark === null) {
return Extension.systemColorStateManager.loadState();
} else if (Extension.wasLastColorSchemeDark !== isDark) {
Extension.wasLastColorSchemeDark = isDark;
return Extension.systemColorStateManager.saveState();
}
}
static alarmListener = (alarm) => {
if (alarm.name === Extension.ALARM_NAME) {
Extension.loadData().then(() =>
Extension.handleAutomationCheck()
);
}
};
static isExtensionSwitchedOn() {
return (
Extension.autoState === "turn-on" ||
Extension.autoState === "scheme-dark" ||
Extension.autoState === "scheme-light" ||
(Extension.autoState === "" && UserStorage.settings.enabled)
);
}
static updateAutoState() {
const {mode, behavior, enabled} = UserStorage.settings.automation;
let isAutoDark;
let nextCheck;
switch (mode) {
case AutomationMode.TIME: {
const {time} = UserStorage.settings;
isAutoDark = isInTimeIntervalLocal(
time.activation,
time.deactivation
);
nextCheck = nextTimeInterval(
time.activation,
time.deactivation
);
break;
}
case AutomationMode.SYSTEM: {
isAutoDark = Extension.wasLastColorSchemeDark;
if (Extension.wasLastColorSchemeDark === null) {
isAutoDark = true;
}
break;
}
case AutomationMode.LOCATION: {
const {latitude, longitude} = UserStorage.settings.location;
if (latitude != null && longitude != null) {
isAutoDark = isNightAtLocation(latitude, longitude);
nextCheck = nextTimeChangeAtLocation(
latitude,
longitude
);
}
break;
}
case AutomationMode.NONE:
break;
}
let state = "";
if (enabled) {
if (behavior === "OnOff") {
state = isAutoDark ? "turn-on" : "turn-off";
} else if (behavior === "Scheme") {
state = isAutoDark ? "scheme-dark" : "scheme-light";
}
}
Extension.autoState = state;
if (nextCheck) {
if (nextCheck < Date.now()) {
logWarn(
`Alarm is set in the past: ${nextCheck}. The time is: ${new Date()}. ISO: ${new Date().toISOString()}`
);
} else {
chrome.alarms.create(Extension.ALARM_NAME, {
when: nextCheck
});
}
}
}
static wakeInterval = -1;
static runWakeDetector() {
const WAKE_CHECK_INTERVAL = getDuration({minutes: 1});
const WAKE_CHECK_INTERVAL_ERROR = getDuration({seconds: 10});
if (this.wakeInterval >= 0) {
clearInterval(this.wakeInterval);
}
let lastRun = Date.now();
this.wakeInterval = setInterval(() => {
const now = Date.now();
if (
now - lastRun >
WAKE_CHECK_INTERVAL + WAKE_CHECK_INTERVAL_ERROR
) {
Extension.handleAutomationCheck();
}
lastRun = now;
}, WAKE_CHECK_INTERVAL);
}
static async start() {
Extension.init();
await Promise.all([
ConfigManager.load({local: true}),
Extension.MV3syncSystemColorStateManager(null),
UserStorage.loadSettings()
]);
if (
UserStorage.settings.enableContextMenus &&
!Extension.registeredContextMenus
) {
chrome.permissions.contains(
{permissions: ["contextMenus"]},
(permitted) => {
if (permitted) {
Extension.registerContextMenus();
}
}
);
}
if (UserStorage.settings.syncSitesFixes) {
await ConfigManager.load({local: false});
}
Extension.updateAutoState();
Extension.runWakeDetector();
Extension.onAppToggle();
if (Extension.isFirstLoad) {
TabManager.updateContentScript({
runOnProtectedPages:
UserStorage.settings.enableForProtectedPages
});
}
UserStorage.settings.fetchNews && Newsmaker.subscribe();
Extension.startBarrier.resolve();
}
static getMessengerAdapter() {
return {
collect: async () => {
return await Extension.collectData();
},
collectDevToolsData: async () => {
return await Extension.collectDevToolsData();
},
changeSettings: Extension.changeSettings,
setTheme: Extension.setTheme,
toggleActiveTab: Extension.toggleActiveTab,
markNewsAsRead: Newsmaker.markAsRead,
markNewsAsDisplayed: Newsmaker.markAsDisplayed,
loadConfig: ConfigManager.load,
applyDevDynamicThemeFixes: DevTools.applyDynamicThemeFixes,
resetDevDynamicThemeFixes: DevTools.resetDynamicThemeFixes,
applyDevInversionFixes: DevTools.applyInversionFixes,
resetDevInversionFixes: DevTools.resetInversionFixes,
applyDevStaticThemes: DevTools.applyStaticThemes,
resetDevStaticThemes: DevTools.resetStaticThemes,
hideHighlights: UIHighlights.hideHighlights
};
}
static onCommandInternal = async (
command,
tabId,
frameId,
frameURL
) => {
if (Extension.startBarrier.isPending()) {
await Extension.startBarrier.entry();
}
Extension.stateManager.loadState();
switch (command) {
case "toggle":
Extension.changeSettings({
enabled: !Extension.isExtensionSwitchedOn(),
automation: {
...UserStorage.settings.automation,
...{enabled: false}
}
});
break;
case "addSite": {
async function scriptPDF(tabId, frameId) {
if (
!(
Number.isInteger(tabId) &&
Number.isInteger(frameId)
)
) {
return false;
}
function detectPDF() {
if (document.body.childElementCount !== 1) {
return false;
}
const {nodeName, type} =
document.body.childNodes[0];
return (
nodeName === "EMBED" &&
type === "application/pdf"
);
}
{
return (
(
await chrome.scripting.executeScript({
target: {tabId, frameIds: [frameId]},
func: detectPDF
})
)[0].result || false
);
}
}
const pdf = async () =>
isPDF(frameURL || (await TabManager.getActiveTabURL()));
if ((await scriptPDF(tabId, frameId)) || (await pdf())) {
Extension.changeSettings({
enableForPDF: !UserStorage.settings.enableForPDF
});
} else {
Extension.toggleActiveTab();
}
break;
}
case "switchEngine": {
const engines = Object.values(ThemeEngine);
const index = engines.indexOf(
UserStorage.settings.theme.engine
);
const next = engines[(index + 1) % engines.length];
Extension.setTheme({engine: next});
break;
}
}
};
static onCommand = debounce(75, Extension.onCommandInternal);
static registerContextMenus() {
chrome.contextMenus.onClicked.addListener(
async ({menuItemId, frameId, frameUrl, pageUrl}, tab) =>
Extension.onCommand(
menuItemId,
(tab && tab.id) || null,
frameId || null,
frameUrl || pageUrl
)
);
chrome.contextMenus.removeAll(() => {
Extension.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: "toggle",
parentId: "DarkReader-top",
title: msgToggle || "Toggle everywhere"
});
chrome.contextMenus.create({
id: "addSite",
parentId: "DarkReader-top",
title: msgAddSite || "Toggle for current site"
});
chrome.contextMenus.create({
id: "switchEngine",
parentId: "DarkReader-top",
title: msgSwitchEngine || "Switch engine"
});
Extension.registeredContextMenus = true;
}
);
});
}
static async getShortcuts() {
const commands = await getCommands();
return commands.reduce(
(map, cmd) => Object.assign(map, {[cmd.name]: cmd.shortcut}),
{}
);
}
static async collectData() {
await Extension.loadData();
const [
news,
shortcuts,
activeTab,
isAllowedFileSchemeAccess,
uiHighlights
] = await Promise.all([
Newsmaker.getLatest(),
Extension.getShortcuts(),
Extension.getActiveTabInfo(),
new Promise((r) =>
chrome.extension.isAllowedFileSchemeAccess(r)
),
UIHighlights.getHighlightsToShow()
]);
return {
isEnabled: Extension.isExtensionSwitchedOn(),
isReady: true,
isAllowedFileSchemeAccess,
settings: UserStorage.settings,
news,
shortcuts,
colorScheme: ConfigManager.COLOR_SCHEMES_RAW,
forcedScheme:
Extension.autoState === "scheme-dark"
? "dark"
: Extension.autoState === "scheme-light"
? "light"
: null,
activeTab,
uiHighlights
};
}
static async collectDevToolsData() {
const [dynamicFixesText, filterFixesText, staticThemesText] =
await Promise.all([
DevTools.getDynamicThemeFixesText(),
DevTools.getInversionFixesText(),
DevTools.getStaticThemesText()
]);
return {
dynamicFixesText,
filterFixesText,
staticThemesText
};
}
static async getActiveTabInfo() {
await Extension.loadData();
const tab = await getActiveTab();
const url = await TabManager.getTabURL(tab);
const {isInDarkList, isProtected} = Extension.getTabInfo(url);
const isInjected = TabManager.canAccessTab(tab);
const documentId = TabManager.getTabDocumentId(tab);
let isDarkThemeDetected = null;
if (UserStorage.settings.detectDarkTheme) {
isDarkThemeDetected = TabManager.isTabDarkThemeDetected(tab);
}
const id = (tab && tab.id) || null;
return {
id,
documentId,
url,
isInDarkList,
isProtected,
isInjected,
isDarkThemeDetected
};
}
static async getConnectionMessage(
tabURL,
url,
isTopFrame,
topFrameHasDarkTheme
) {
await Extension.loadData();
return Extension.getTabMessage(
tabURL,
url,
isTopFrame,
topFrameHasDarkTheme
);
}
static async loadData() {
Extension.init();
await Promise.all([
Extension.stateManager.loadState(),
UserStorage.loadSettings()
]);
}
static onColorSchemeChange = async (isDark) => {
if (Extension.wasLastColorSchemeDark === isDark) {
return;
}
Extension.wasLastColorSchemeDark = isDark;
Extension.MV3syncSystemColorStateManager(isDark);
await Extension.loadData();
if (
UserStorage.settings.automation.mode !== AutomationMode.SYSTEM
) {
return;
}
Extension.handleAutomationCheck();
};
static handleAutomationCheck = () => {
Extension.updateAutoState();
const isSwitchedOn = Extension.isExtensionSwitchedOn();
if (
Extension.wasEnabledOnLastCheck === null ||
Extension.wasEnabledOnLastCheck !== isSwitchedOn ||
Extension.autoState === "scheme-dark" ||
Extension.autoState === "scheme-light"
) {
Extension.wasEnabledOnLastCheck = isSwitchedOn;
Extension.onAppToggle();
TabManager.sendMessage();
Extension.reportChanges();
Extension.stateManager.saveState();
}
};
static async changeSettings($settings, onlyUpdateActiveTab = false) {
const promises = [];
const prev = {...UserStorage.settings};
UserStorage.set($settings);
if (
prev.enabled !== UserStorage.settings.enabled ||
prev.automation.enabled !==
UserStorage.settings.automation.enabled ||
prev.automation.mode !== UserStorage.settings.automation.mode ||
prev.automation.behavior !==
UserStorage.settings.automation.behavior ||
prev.time.activation !== UserStorage.settings.time.activation ||
prev.time.deactivation !==
UserStorage.settings.time.deactivation ||
prev.location.latitude !==
UserStorage.settings.location.latitude ||
prev.location.longitude !==
UserStorage.settings.location.longitude
) {
Extension.updateAutoState();
Extension.onAppToggle();
}
if (prev.syncSettings !== UserStorage.settings.syncSettings) {
const promise = UserStorage.saveSyncSetting(
UserStorage.settings.syncSettings
);
promises.push(promise);
}
if (
Extension.isExtensionSwitchedOn() &&
$settings.changeBrowserTheme != null &&
prev.changeBrowserTheme !== $settings.changeBrowserTheme
) {
if ($settings.changeBrowserTheme) {
setWindowTheme(UserStorage.settings.theme);
} else {
resetWindowTheme();
}
}
if (prev.fetchNews !== UserStorage.settings.fetchNews) {
UserStorage.settings.fetchNews
? Newsmaker.subscribe()
: Newsmaker.unSubscribe();
}
if (
prev.enableContextMenus !==
UserStorage.settings.enableContextMenus
) {
if (UserStorage.settings.enableContextMenus) {
Extension.registerContextMenus();
} else {
chrome.contextMenus.removeAll();
}
}
const promise = Extension.onSettingsChanged(onlyUpdateActiveTab);
promises.push(promise);
await Promise.all(promises);
}
static setTheme($theme) {
UserStorage.set({
theme: {...UserStorage.settings.theme, ...$theme}
});
if (
Extension.isExtensionSwitchedOn() &&
UserStorage.settings.changeBrowserTheme
) {
setWindowTheme(UserStorage.settings.theme);
}
Extension.onSettingsChanged();
}
static async reportChanges() {
const info = await Extension.collectData();
Messenger.reportChanges(info);
}
static async toggleActiveTab() {
const settings = UserStorage.settings;
const tab = await Extension.getActiveTabInfo();
if (!tab) {
return;
}
const {url} = tab;
const isInDarkList = ConfigManager.isURLInDarkList(url);
const host = getURLHostOrProtocol(url);
function getToggledList(sourceList) {
const list = sourceList.slice();
let index = list.indexOf(host);
if (index < 0 && host.startsWith("www.")) {
const noWwwHost = host.substring(4);
index = list.indexOf(noWwwHost);
}
if (index < 0) {
list.push(host);
} else {
list.splice(index, 1);
}
return list;
}
const darkThemeDetected =
settings.enabledByDefault &&
settings.detectDarkTheme &&
tab.isDarkThemeDetected;
if (
!settings.enabledByDefault ||
isInDarkList ||
darkThemeDetected
) {
const toggledList = getToggledList(settings.enabledFor);
Extension.changeSettings({enabledFor: toggledList}, true);
return;
}
if (
settings.enabledByDefault &&
settings.enabledFor.includes(host)
) {
const enabledFor = getToggledList(settings.enabledFor);
const disabledFor = getToggledList(settings.disabledFor);
Extension.changeSettings({enabledFor, disabledFor}, true);
return;
}
const toggledList = getToggledList(settings.disabledFor);
Extension.changeSettings({disabledFor: toggledList}, true);
}
static onAppToggle() {
if (Extension.isExtensionSwitchedOn()) {
IconManager.setActive();
} else {
IconManager.setInactive();
}
if (UserStorage.settings.changeBrowserTheme) {
if (
Extension.isExtensionSwitchedOn() &&
Extension.autoState !== "scheme-light"
) {
setWindowTheme(UserStorage.settings.theme);
} else {
resetWindowTheme();
}
}
}
static async onSettingsChanged(onlyUpdateActiveTab = false) {
await Extension.loadData();
Extension.wasEnabledOnLastCheck = Extension.isExtensionSwitchedOn();
TabManager.sendMessage(onlyUpdateActiveTab);
Extension.saveUserSettings();
Extension.reportChanges();
Extension.stateManager.saveState();
}
static getTabInfo(tabURL) {
const isInDarkList = ConfigManager.isURLInDarkList(tabURL);
const isProtected = !canInjectScript(tabURL);
return {
isInDarkList,
isProtected
};
}
static getTabMessage = (
tabURL,
url,
isTopFrame,
topFrameHasDarkTheme
) => {
const settings = UserStorage.settings;
const tabInfo = Extension.getTabInfo(tabURL);
if (
Extension.isExtensionSwitchedOn() &&
isURLEnabled(tabURL, settings, tabInfo) &&
!topFrameHasDarkTheme
) {
const custom = settings.customThemes.find(({url: urlList}) =>
isURLInList(tabURL, urlList)
);
const preset = custom
? null
: settings.presets.find(({urls}) =>
isURLInList(tabURL, urls)
);
let theme = custom
? custom.theme
: preset
? preset.theme
: settings.theme;
if (
Extension.autoState === "scheme-dark" ||
Extension.autoState === "scheme-light"
) {
const mode = Extension.autoState === "scheme-dark" ? 1 : 0;
theme = {...theme, mode};
}
const detectDarkTheme =
isTopFrame &&
settings.detectDarkTheme &&
!isURLInList(tabURL, settings.enabledFor) &&
!isPDF(tabURL);
const detectorHints = detectDarkTheme
? getDetectorHintsFor(
url,
ConfigManager.DETECTOR_HINTS_RAW,
ConfigManager.DETECTOR_HINTS_INDEX
)
: null;
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 ThemeEngine.cssFilter: {
return {
type: MessageTypeBGtoCS.ADD_CSS_FILTER,
data: {
css: createCSSFilterStyleSheet(
theme,
url,
isTopFrame,
ConfigManager.INVERSION_FIXES_RAW,
ConfigManager.INVERSION_FIXES_INDEX
),
detectDarkTheme,
detectorHints
}
};
}
case ThemeEngine.svgFilter: {
return {
type: MessageTypeBGtoCS.ADD_SVG_FILTER,
data: {
css: createSVGFilterStylesheet(
theme,
url,
isTopFrame,
ConfigManager.INVERSION_FIXES_RAW,
ConfigManager.INVERSION_FIXES_INDEX
),
svgMatrix: getSVGFilterMatrixValue(theme),
svgReverseMatrix:
getSVGReverseFilterMatrixValue(),
detectDarkTheme,
detectorHints
}
};
}
case ThemeEngine.staticTheme: {
return {
type: MessageTypeBGtoCS.ADD_STATIC_THEME,
data: {
css:
theme.stylesheet && theme.stylesheet.trim()
? theme.stylesheet
: createStaticStylesheet(
theme,
url,
isTopFrame,
ConfigManager.STATIC_THEMES_RAW,
ConfigManager.STATIC_THEMES_INDEX
),
detectDarkTheme: settings.detectDarkTheme,
detectorHints
}
};
}
case ThemeEngine.dynamicTheme: {
const fixes = getDynamicThemeFixesFor(
url,
isTopFrame,
ConfigManager.DYNAMIC_THEME_FIXES_RAW,
ConfigManager.DYNAMIC_THEME_FIXES_INDEX,
UserStorage.settings.enableForPDF
);
return {
type: MessageTypeBGtoCS.ADD_DYNAMIC_THEME,
data: {
theme,
fixes,
isIFrame: !isTopFrame,
detectDarkTheme,
detectorHints
}
};
}
default:
throw new Error(`Unknown engine ${theme.engine}`);
}
}
return {
type: MessageTypeBGtoCS.CLEAN_UP
};
};
static async saveUserSettings() {
await UserStorage.saveSettings();
}
}
Extension.start();
const welcome = ` /''''\\
(0)==(0)
/__||||__\\
Welcome to Dark Reader!`;
console.log(welcome);
{
chrome.runtime.onInstalled.addListener(async () => {
Extension.isFirstLoad = true;
});
keepListeningToEvents();
}
{
chrome.runtime.onInstalled.addListener(({reason}) => {
if (reason === "install") {
chrome.tabs.create({url: getHelpURL()});
}
});
chrome.runtime.setUninstallURL(UNINSTALL_URL);
}
function writeInstallationVersion(storage, details) {
storage.get({installation: {version: ""}}, (data) => {
if (data?.installation?.version) {
return;
}
storage.set({
installation: {
date: Date.now(),
reason: details.reason,
version:
details.previousVersion ??
chrome.runtime.getManifest().version
}
});
});
}
chrome.runtime.onInstalled.addListener((details) => {
writeInstallationVersion(chrome.storage.local, details);
writeInstallationVersion(chrome.storage.sync, details);
});
})();