Repository navigation
Expand file tree
/
Copy pathdarkmode.js
More file actions
175 lines (161 loc) · 6.31 KB
/
Copy pathdarkmode.js
File metadata and controls
175 lines (161 loc) · 6.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
(() => {
const STYLE_ID = "superlevels-darkmode";
// CSS approach: invert the whole page, then re-invert media so images/videos look normal
function buildCSS(brightness) {
const b = brightness / 100;
return `
html.superlevels-dark {
filter: invert(1) hue-rotate(180deg) brightness(${b}) !important;
background: #fff !important;
}
html.superlevels-dark img,
html.superlevels-dark video,
html.superlevels-dark canvas,
html.superlevels-dark svg image,
html.superlevels-dark [style*="background-image"],
html.superlevels-dark iframe {
filter: invert(1) hue-rotate(180deg) !important;
}
/* Note: do NOT include <picture> here. <picture> wraps an <img> which is
already re-inverted above; adding picture causes triple inversion and
images render inverted/washed out (e.g. nos.nl tile images vanish). */
/* Reddit renders post video/images inside custom-element players that use
a shadow root (e.g. <shreddit-player-2>, <shreddit-player>). The page
invert reaches the shadow <video>, but the "video" re-invert selector
above can't cross the shadow boundary, so the video shows up inverted.
Re-invert the host element instead: that cancels the page invert for the
whole shadow subtree (video + controls) at once. */
html.superlevels-dark shreddit-player-2,
html.superlevels-dark shreddit-player,
html.superlevels-dark shreddit-media-player {
filter: invert(1) hue-rotate(180deg) !important;
}
/* nos.nl article tiles put a dark gradient scrim over the image (behind the
white title). The page invert turns that scrim white and washes out the
image, so re-invert the scrim (invert twice = original) to keep it dark.
Match on the stable class-name suffixes, not the hashed module chunk. */
html.superlevels-dark [class*="ItemTile"] [class*="imageWrapper"]::before,
html.superlevels-dark [class*="ItemTile"] [class*="imageWrapper"]::after,
html.superlevels-dark [class*="ItemTile"] [class*="__content"],
html.superlevels-dark [class*="ItemTile"] [class*="__content"]::before,
html.superlevels-dark [class*="ItemTile"] [class*="__content"]::after {
filter: invert(1) hue-rotate(180deg) !important;
}
/* Fix common elements that break */
html.superlevels-dark input,
html.superlevels-dark textarea,
html.superlevels-dark select {
background-color: inherit !important;
color: inherit !important;
}
`;
}
function applyDarkMode(enabled, brightness) {
let style = document.getElementById(STYLE_ID);
if (enabled) {
if (!style) {
style = document.createElement("style");
style.id = STYLE_ID;
(document.head || document.documentElement).appendChild(style);
}
style.textContent = buildCSS(brightness);
document.documentElement.classList.add("superlevels-dark");
} else {
document.documentElement.classList.remove("superlevels-dark");
if (style) style.remove();
}
}
// Detect if the page's own styles already render it dark
function parseColor(str) {
if (!str) return null;
const m = str.match(/rgba?\(([^)]+)\)/);
if (!m) return null;
const parts = m[1].split(",").map((s) => parseFloat(s.trim()));
return {
r: parts[0],
g: parts[1],
b: parts[2],
a: parts[3] !== undefined ? parts[3] : 1,
};
}
function luminance(c) {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) / 255;
}
function pageBgIsDark() {
// Temporarily strip our class so getComputedStyle reflects the page's own bg
const html = document.documentElement;
const had = html.classList.contains("superlevels-dark");
if (had) html.classList.remove("superlevels-dark");
let bg = null;
if (document.body) {
const c = parseColor(getComputedStyle(document.body).backgroundColor);
if (c && c.a > 0.5) bg = c;
}
if (!bg) {
const c = parseColor(getComputedStyle(html).backgroundColor);
if (c && c.a > 0.5) bg = c;
}
if (had) html.classList.add("superlevels-dark");
if (!bg) return false;
return luminance(bg) < 0.35;
}
// Get hostname for per-site storage
const host = location.hostname;
const storageKey = "darkmode_" + host;
const globalKey = "darkmode_global";
const pageDarkKey = "darkmode_pageIsDark_" + host;
// Load state as early as possible to prevent flash
chrome.storage.local.get(
[storageKey, globalKey, "darkmode_brightness", pageDarkKey],
(data) => {
const siteState = data[storageKey];
const globalState = data[globalKey];
const enabled =
siteState !== undefined ? siteState : globalState || false;
const brightness = data.darkmode_brightness || 100;
if (!enabled) return;
// Skip immediately if we've previously detected this site as natively dark
if (data[pageDarkKey]) return;
applyDarkMode(true, brightness);
// Re-verify once body styles are computed
const recheck = () => {
if (pageBgIsDark()) {
applyDarkMode(false, 0);
chrome.storage.local.set({ [pageDarkKey]: true });
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", recheck, { once: true });
} else {
recheck();
}
}
);
// Listen for toggle messages from popup
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === "darkmode_toggle") {
if (!msg.enabled) {
applyDarkMode(false, 0);
chrome.storage.local.remove(pageDarkKey);
sendResponse({ ok: true, skipped: false });
return;
}
// Enabling: skip if the page is already dark by its own styles
if (pageBgIsDark()) {
applyDarkMode(false, 0);
chrome.storage.local.set({ [pageDarkKey]: true });
sendResponse({ ok: true, skipped: true });
} else {
chrome.storage.local.remove(pageDarkKey);
applyDarkMode(true, msg.brightness || 100);
sendResponse({ ok: true, skipped: false });
}
}
if (msg.type === "darkmode_query") {
sendResponse({
active: document.documentElement.classList.contains("superlevels-dark"),
host: host,
});
}
});
})();