Skip to content

Commit 0fad2a2

Browse files
authored
Detect website's dark theme (darkreader#7995)
- Disable Dark Reader styles during the check. - Check Root and Body backgrounds. - Works for Dynamic, Filter and Static modes. - No way to disable per website yet.
1 parent f18c3be commit 0fad2a2

10 files changed

Lines changed: 162 additions & 22 deletions

File tree

src/background/extension.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,8 @@ export class Extension implements ExtensionState {
594594
const mode = this.autoState === 'scheme-dark' ? 1 : 0;
595595
theme = {...theme, mode};
596596
}
597+
const isIFrame = frameURL != null;
598+
const detectDarkTheme = !isIFrame && this.user.settings.detectDarkTheme;
597599

598600
logInfo(`Creating CSS for url: ${url}`);
599601
logInfo(`Custom theme ${custom ? 'was found' : 'was not found'}, Preset theme ${preset ? 'was found' : 'was not found'}
@@ -602,14 +604,20 @@ export class Extension implements ExtensionState {
602604
case ThemeEngines.cssFilter: {
603605
return {
604606
type: MessageType.BG_ADD_CSS_FILTER,
605-
data: createCSSFilterStylesheet(theme, url, frameURL, this.config.INVERSION_FIXES_RAW, this.config.INVERSION_FIXES_INDEX),
607+
data: {
608+
css: createCSSFilterStylesheet(theme, url, frameURL, this.config.INVERSION_FIXES_RAW, this.config.INVERSION_FIXES_INDEX),
609+
detectDarkTheme,
610+
},
606611
};
607612
}
608613
case ThemeEngines.svgFilter: {
609614
if (isFirefox) {
610615
return {
611616
type: MessageType.BG_ADD_CSS_FILTER,
612-
data: createSVGFilterStylesheet(theme, url, frameURL, this.config.INVERSION_FIXES_RAW, this.config.INVERSION_FIXES_INDEX),
617+
data: {
618+
css: createSVGFilterStylesheet(theme, url, frameURL, this.config.INVERSION_FIXES_RAW, this.config.INVERSION_FIXES_INDEX),
619+
detectDarkTheme,
620+
},
613621
};
614622
}
615623
return {
@@ -618,25 +626,31 @@ export class Extension implements ExtensionState {
618626
css: createSVGFilterStylesheet(theme, url, frameURL, this.config.INVERSION_FIXES_RAW, this.config.INVERSION_FIXES_INDEX),
619627
svgMatrix: getSVGFilterMatrixValue(theme),
620628
svgReverseMatrix: getSVGReverseFilterMatrixValue(),
629+
detectDarkTheme,
621630
},
622631
};
623632
}
624633
case ThemeEngines.staticTheme: {
625634
return {
626635
type: MessageType.BG_ADD_STATIC_THEME,
627-
data: theme.stylesheet && theme.stylesheet.trim() ?
628-
theme.stylesheet :
629-
createStaticStylesheet(theme, url, frameURL, this.config.STATIC_THEMES_RAW, this.config.STATIC_THEMES_INDEX),
636+
data: {
637+
css: theme.stylesheet && theme.stylesheet.trim() ?
638+
theme.stylesheet :
639+
createStaticStylesheet(theme, url, frameURL, this.config.STATIC_THEMES_RAW, this.config.STATIC_THEMES_INDEX),
640+
detectDarkTheme: this.user.settings.detectDarkTheme,
641+
},
630642
};
631643
}
632644
case ThemeEngines.dynamicTheme: {
633-
const filter = {...theme};
634-
delete filter.engine;
635645
const fixes = getDynamicThemeFixesFor(url, frameURL, this.config.DYNAMIC_THEME_FIXES_RAW, this.config.DYNAMIC_THEME_FIXES_INDEX, this.user.settings.enableForPDF);
636-
const isIFrame = frameURL != null;
637646
return {
638647
type: MessageType.BG_ADD_DYNAMIC_THEME,
639-
data: {filter, fixes, isIFrame},
648+
data: {
649+
theme,
650+
fixes,
651+
isIFrame,
652+
detectDarkTheme,
653+
},
640654
};
641655
}
642656
default: {

src/defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,5 @@ export const DEFAULT_SETTINGS: UserSettings = {
7878
enableForPDF: true,
7979
enableForProtectedPages: false,
8080
enableContextMenus: false,
81+
detectDarkTheme: false,
8182
};

src/definitions.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export interface UserSettings {
101101
enableForPDF: boolean;
102102
enableForProtectedPages: boolean;
103103
enableContextMenus: boolean;
104+
detectDarkTheme: boolean;
104105
}
105106

106107
export interface TimeSettings {

src/inject/detector.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {parse, getSRGBLightness} from '../utils/color';
2+
3+
function hasBuiltInDarkTheme() {
4+
const drStyles = document.querySelectorAll('.darkreader') as NodeListOf<HTMLStyleElement & {disabled: boolean}>;
5+
drStyles.forEach((style) => style.disabled = true);
6+
7+
const rootColor = parse(getComputedStyle(document.documentElement).backgroundColor);
8+
const bodyColor = document.body ? parse(getComputedStyle(document.body).backgroundColor) : {r: 0, g: 0, b: 0, a: 0};
9+
const rootLightness = (1 - rootColor.a) + rootColor.a * getSRGBLightness(rootColor.r, rootColor.g, rootColor.b);
10+
const finalLightness = (1 - bodyColor.a) * rootLightness + bodyColor.a * getSRGBLightness(bodyColor.r, bodyColor.g, bodyColor.b);
11+
const darkThemeDetected = finalLightness < 0.5;
12+
13+
drStyles.forEach((style) => style.disabled = false);
14+
return darkThemeDetected;
15+
}
16+
17+
function runCheck(callback: (hasDarkTheme: boolean) => void) {
18+
const darkThemeDetected = hasBuiltInDarkTheme();
19+
callback(darkThemeDetected);
20+
}
21+
22+
function hasSomeStyle() {
23+
if (document.documentElement.style.backgroundColor || (document.body && document.body.style.backgroundColor)) {
24+
return true;
25+
}
26+
for (const style of document.styleSheets) {
27+
if (style && style.ownerNode && !(style.ownerNode as HTMLElement).classList.contains('darkreader')) {
28+
return true;
29+
}
30+
}
31+
return false;
32+
}
33+
34+
let observer: MutationObserver;
35+
let readyStateListener: () => void;
36+
37+
export function runDarkThemeDetector(callback: (hasDarkTheme: boolean) => void) {
38+
stopDarkThemeDetector();
39+
if (document.body && hasSomeStyle()) {
40+
runCheck(callback);
41+
return;
42+
}
43+
44+
observer = new MutationObserver(() => {
45+
if (document.body && hasSomeStyle()) {
46+
stopDarkThemeDetector();
47+
runCheck(callback);
48+
}
49+
});
50+
observer.observe(document.documentElement, {childList: true});
51+
52+
if (document.readyState !== 'complete') {
53+
readyStateListener = () => {
54+
if (document.readyState === 'complete') {
55+
stopDarkThemeDetector();
56+
runCheck(callback);
57+
}
58+
};
59+
document.addEventListener('readystatechange', readyStateListener);
60+
}
61+
}
62+
63+
export function stopDarkThemeDetector() {
64+
if (observer) {
65+
observer.disconnect();
66+
observer = null;
67+
}
68+
if (readyStateListener) {
69+
document.removeEventListener('readystatechange', readyStateListener);
70+
readyStateListener = null;
71+
}
72+
}

src/inject/dynamic-theme/image.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {getSVGFilterMatrixValue} from '../../generators/svg-filter';
22
import {bgFetch} from './network';
3+
import {getSRGBLightness} from '../../utils/color';
34
import {loadAsDataURL} from '../../utils/network';
45
import type {FilterConfig} from '../../definitions';
56
import {logInfo, logWarn} from '../../utils/log';
@@ -140,17 +141,15 @@ function analyzeImage(image: HTMLImageElement) {
140141
for (y = 0; y < height; y++) {
141142
for (x = 0; x < width; x++) {
142143
i = 4 * (y * width + x);
143-
r = d[i + 0] / 255;
144-
g = d[i + 1] / 255;
145-
b = d[i + 2] / 255;
146-
a = d[i + 3] / 255;
144+
r = d[i + 0];
145+
g = d[i + 1];
146+
b = d[i + 2];
147+
a = d[i + 3];
147148

148-
if (a < TRANSPARENT_ALPHA_THRESHOLD) {
149+
if (a / 255 < TRANSPARENT_ALPHA_THRESHOLD) {
149150
transparentPixelsCount++;
150151
} else {
151-
// Use sRGB to determine the `pixel Lightness`
152-
// https://en.wikipedia.org/wiki/Relative_luminance
153-
l = 0.2126 * r + 0.7152 * g + 0.0722 * b;
152+
l = getSRGBLightness(r, g, b);
154153
if (l < DARK_LIGHTNESS_THRESHOLD) {
155154
darkPixelsCount++;
156155
}

src/inject/index.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {createOrUpdateStyle, removeStyle} from './style';
22
import {createOrUpdateSVGFilter, removeSVGFilter} from './svg-filter';
3+
import {runDarkThemeDetector, stopDarkThemeDetector} from './detector';
34
import {createOrUpdateDynamicTheme, removeDynamicTheme, cleanDynamicThemeCache} from './dynamic-theme';
45
import {logInfo, logWarn} from '../utils/log';
56
import {watchForColorSchemeChange} from './utils/watch-color-scheme';
@@ -22,6 +23,7 @@ function cleanup() {
2223
removeEventListener('freeze', onFreeze);
2324
removeEventListener('resume', onResume);
2425
cleanDynamicThemeCache();
26+
stopDarkThemeDetector();
2527
if (colorSchemeWatcher) {
2628
colorSchemeWatcher.disconnect();
2729
colorSchemeWatcher = null;
@@ -58,22 +60,44 @@ function onMessage({type, data}: Message) {
5860
switch (type) {
5961
case MessageType.BG_ADD_CSS_FILTER:
6062
case MessageType.BG_ADD_STATIC_THEME: {
61-
const css = data;
63+
const {css, detectDarkTheme} = data;
6264
removeDynamicTheme();
6365
createOrUpdateStyle(css, type === MessageType.BG_ADD_STATIC_THEME ? 'static' : 'filter');
66+
if (detectDarkTheme) {
67+
runDarkThemeDetector((hasDarkTheme) => {
68+
if (hasDarkTheme) {
69+
removeStyle();
70+
}
71+
});
72+
}
6473
break;
6574
}
6675
case MessageType.BG_ADD_SVG_FILTER: {
67-
const {css, svgMatrix, svgReverseMatrix} = data;
76+
const {css, svgMatrix, svgReverseMatrix, detectDarkTheme} = data;
6877
removeDynamicTheme();
6978
createOrUpdateSVGFilter(svgMatrix, svgReverseMatrix);
7079
createOrUpdateStyle(css, 'filter');
80+
if (detectDarkTheme) {
81+
runDarkThemeDetector((hasDarkTheme) => {
82+
if (hasDarkTheme) {
83+
removeStyle();
84+
removeSVGFilter();
85+
}
86+
});
87+
}
7188
break;
7289
}
7390
case MessageType.BG_ADD_DYNAMIC_THEME: {
74-
const {filter, fixes, isIFrame} = data;
91+
const {theme, fixes, isIFrame, detectDarkTheme} = data;
7592
removeStyle();
76-
createOrUpdateDynamicTheme(filter, fixes, isIFrame);
93+
createOrUpdateDynamicTheme(theme, fixes, isIFrame);
94+
if (detectDarkTheme) {
95+
runDarkThemeDetector((hasDarkTheme) => {
96+
if (hasDarkTheme) {
97+
removeDynamicTheme();
98+
}
99+
});
100+
}
77101
break;
78102
}
79103
case MessageType.BG_EXPORT_CSS: {
@@ -85,6 +109,7 @@ function onMessage({type, data}: Message) {
85109
removeStyle();
86110
removeSVGFilter();
87111
removeDynamicTheme();
112+
stopDarkThemeDetector();
88113
break;
89114
}
90115
case MessageType.BG_RELOAD:

src/inject/style.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export function createOrUpdateStyle(css: string, type: string) {
77
document.documentElement.setAttribute('data-darkreader-mode', type);
88
const style = document.createElement('style');
99
style.id = 'dark-reader-style';
10+
style.classList.add('darkreader');
1011
style.type = 'text/css';
1112
style.textContent = css;
1213
target.appendChild(style);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {m} from 'malevic';
2+
import CheckButton from '../check-button';
3+
import type {ViewProps} from '../types';
4+
5+
export default function DetectDarkTheme(props: ViewProps) {
6+
function onDetectDarkThemeChange(checked: boolean) {
7+
props.actions.changeSettings({detectDarkTheme: checked});
8+
}
9+
10+
return (
11+
<CheckButton
12+
checked={props.data.settings.detectDarkTheme}
13+
label="Detect dark theme"
14+
description={props.data.settings.detectDarkTheme ?
15+
`Will not override website's dark theme` :
16+
`Will override website's dark theme`}
17+
onChange={onDetectDarkThemeChange}
18+
/>
19+
);
20+
}

src/ui/popup/settings-page/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import DevToolsGroup from './devtools';
55
import ManageSettingsButton from './mange-settings-button';
66
import SiteListButton from './site-list-button';
77
import EnabledByDefaultGroup from './enabled-by-default';
8+
import DetectDarkThemeGroup from './detect-dark-theme';
89
import ChangeBrowserTheme from './change-browser-theme';
910
import ContextMenusGroup from './context-menus';
1011
import Version from './version';
@@ -26,6 +27,7 @@ export default function SettingsPage(props: SettingsPageProps) {
2627
<AutomationButton onClick={props.onAutomationNavClick} />
2728
<ContextMenusGroup {...props} />
2829
<ManageSettingsButton onClick={props.onManageSettingsClick} />
30+
<DetectDarkThemeGroup {...props} />
2931
<Version />
3032
</section>
3133
);

src/utils/color.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ export function lowerCalcExpression(color: string): string {
263263
// the calc(...) expression.
264264
let searchIndex = 0;
265265

266-
const replaceBetweenIndices = (start: number, end:number, replacement: string) => {
266+
const replaceBetweenIndices = (start: number, end: number, replacement: string) => {
267267
color = color.substring(0, start) + replacement + color.substring(end);
268268
};
269269

@@ -533,3 +533,8 @@ const systemColors: Map<string, number> = new Map(Object.entries({
533533
WindowText: 0x000000,
534534
'-webkit-focus-ring-color': 0xe59700
535535
}).map(([key, value]) => [key.toLowerCase(), value] as [string, number]));
536+
537+
// https://en.wikipedia.org/wiki/Relative_luminance
538+
export function getSRGBLightness(r: number, g: number, b: number) {
539+
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
540+
}

0 commit comments

Comments
 (0)