Selectively toggle dark mode for any website
// ==UserScript==
// @name Darkify
// @namespace http://yifangu.com
// @license MIT
// @version 2024-02-27
// @description Selectively toggle dark mode for any website
// @author You
// @match *://*/*
// @run-at document-start
// @inject-into content
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.info
// ==/UserScript==
(async function () {
"use strict";
if (window.top !== window.self) {
return;
}
const ver = GM.info.script.version;
const firstRunReadme = [
"This is your first time running Darkify",
"Press CTRL+SHIFT+D to toggle on/off dark mode at a given site.",
"For mobile devices, hold down three fingers for 3 seconds to toggle on/off dark mode.",
`Version ${ver}`,
].join("\n");
const upgradeReadme = [
`Darkify has been upgraded to version ${ver}`
].join("\n");
const defaultConfig = {
enabled: false,
};
const savedVer = await GM.getValue("version");
if (savedVer === undefined) {
// first run
alert(firstRunReadme);
await GM.setValue("version", ver);
} else if (savedVer !== ver) {
alert(upgradeReadme);
await GM.setValue("version", ver);
}
function hex(s) {
return s.split("").map((c) => c.charCodeAt(0).toString(16)).join("");
}
const loadConfig = async () => {
const siteKey = hex(location.hostname);
const configJson = await GM.getValue(`siteConfig.${siteKey}`, "{}");
let config = defaultConfig;
try {
config = { ...defaultConfig, ...JSON.parse(configJson) };
} catch (e) {
// ignore
}
return config;
};
const saveConfig = async (config) => {
const siteKey = hex(location.hostname);
await GM.setValue(`siteConfig.${siteKey}`, JSON.stringify(config));
}
const config = await loadConfig();
const { enabled } = config;
const toggle = async () => {
await saveConfig({ ...config, enabled: !enabled });
location.reload();
};
window.addEventListener("keydown", async (e) => {
// ctrl + shift + d
if (e.ctrlKey && e.shiftKey && e.key === "D") {
e.preventDefault();
await toggle();
}
});
let toggleTimeoutHandle;
document.addEventListener("touchmove", (e) => {
const num = e.touches.length;
if (num === 3) {
if (toggleTimeoutHandle === undefined) {
toggleTimeoutHandle = setTimeout(async () => {
await toggle();
toggleTimeoutHandle = undefined;
}, 3000);
}
} else {
if (toggleTimeoutHandle !== undefined) {
clearTimeout(toggleTimeoutHandle);
toggleTimeoutHandle = undefined;
}
}
});
document.addEventListener("touchend", () => {
if (toggleTimeoutHandle !== undefined) {
clearTimeout(toggleTimeoutHandle);
toggleTimeoutHandle = undefined;
}
});
if (!enabled) {
return;
}
const css = `
:root {
color-scheme: dark;
background-color: white;
filter: invert(100%) hue-rotate(180deg) !important;
-webkit-font-smoothing: antialiased;
color: black;
}
body {
color-scheme: light;
background-color: #000 !important;
background: linear-gradient(#fff, #fff);
}
img:not([src*="svg"]),video,iframe,embed,object,canvas,picture,.logo,.darkify-ignore {
filter: invert(100%) hue-rotate(180deg) !important;
}
img[src*="svg"]:hover {
background-color: #aaa !important;
}
`;
function darkify() {
const divs = document.querySelectorAll("div");
for (const div of divs) {
if (!div.classList.contains("darkify-ignore")) {
continue;
}
const cs = getComputedStyle(div);
const bg = cs.backgroundImage;
const bs = cs.backgroundSize;
if (bg === "none") {
continue;
}
if (!["cover", "contain"].includes(bs)) {
continue;
}
if (bg.includes(".svg") || bg.includes(".gif")) {
continue;
}
div.classList.add("darkify-ignore");
}
if (document.getElementById("darkify")) {
return;
}
const style = document.createElement("style");
style.id = "darkify";
style.innerHTML = css;
const themeColorMeta = document.createElement("meta");
themeColorMeta.name = "theme-color";
themeColorMeta.content = "#000000";
document.head.appendChild(style);
document.head.appendChild(themeColorMeta);
}
darkify();
document.addEventListener("DOMContentLoaded", darkify);
})();