6854 lines
249 KiB
JavaScript
6854 lines
249 KiB
JavaScript
|
(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);
|
||
|
});
|
||
|
})();
|