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