Darkify

Selectively toggle dark mode for any website

  1. // ==UserScript==
  2. // @name Darkify
  3. // @namespace http://yifangu.com
  4. // @license MIT
  5. // @version 2024-02-27
  6. // @description Selectively toggle dark mode for any website
  7. // @author You
  8. // @match *://*/*
  9. // @run-at document-start
  10. // @inject-into content
  11. // @grant GM.setValue
  12. // @grant GM.getValue
  13. // @grant GM.info
  14. // ==/UserScript==
  15.  
  16. (async function () {
  17. "use strict";
  18. if (window.top !== window.self) {
  19. return;
  20. }
  21. const ver = GM.info.script.version;
  22. const firstRunReadme = [
  23. "This is your first time running Darkify",
  24. "Press CTRL+SHIFT+D to toggle on/off dark mode at a given site.",
  25. "For mobile devices, hold down three fingers for 3 seconds to toggle on/off dark mode.",
  26. `Version ${ver}`,
  27. ].join("\n");
  28.  
  29. const upgradeReadme = [
  30. `Darkify has been upgraded to version ${ver}`
  31. ].join("\n");
  32.  
  33. const defaultConfig = {
  34. enabled: false,
  35. };
  36.  
  37. const savedVer = await GM.getValue("version");
  38. if (savedVer === undefined) {
  39. // first run
  40. alert(firstRunReadme);
  41. await GM.setValue("version", ver);
  42. } else if (savedVer !== ver) {
  43. alert(upgradeReadme);
  44. await GM.setValue("version", ver);
  45. }
  46.  
  47. function hex(s) {
  48. return s.split("").map((c) => c.charCodeAt(0).toString(16)).join("");
  49. }
  50.  
  51. const loadConfig = async () => {
  52. const siteKey = hex(location.hostname);
  53. const configJson = await GM.getValue(`siteConfig.${siteKey}`, "{}");
  54. let config = defaultConfig;
  55. try {
  56. config = { ...defaultConfig, ...JSON.parse(configJson) };
  57. } catch (e) {
  58. // ignore
  59. }
  60. return config;
  61. };
  62.  
  63. const saveConfig = async (config) => {
  64. const siteKey = hex(location.hostname);
  65. await GM.setValue(`siteConfig.${siteKey}`, JSON.stringify(config));
  66. }
  67.  
  68. const config = await loadConfig();
  69. const { enabled } = config;
  70.  
  71. const toggle = async () => {
  72. await saveConfig({ ...config, enabled: !enabled });
  73. location.reload();
  74. };
  75.  
  76. window.addEventListener("keydown", async (e) => {
  77. // ctrl + shift + d
  78. if (e.ctrlKey && e.shiftKey && e.key === "D") {
  79. e.preventDefault();
  80. await toggle();
  81. }
  82. });
  83.  
  84. let toggleTimeoutHandle;
  85. document.addEventListener("touchmove", (e) => {
  86. const num = e.touches.length;
  87. if (num === 3) {
  88. if (toggleTimeoutHandle === undefined) {
  89. toggleTimeoutHandle = setTimeout(async () => {
  90. await toggle();
  91. toggleTimeoutHandle = undefined;
  92. }, 3000);
  93. }
  94. } else {
  95. if (toggleTimeoutHandle !== undefined) {
  96. clearTimeout(toggleTimeoutHandle);
  97. toggleTimeoutHandle = undefined;
  98. }
  99. }
  100. });
  101.  
  102. document.addEventListener("touchend", () => {
  103. if (toggleTimeoutHandle !== undefined) {
  104. clearTimeout(toggleTimeoutHandle);
  105. toggleTimeoutHandle = undefined;
  106. }
  107. });
  108.  
  109. if (!enabled) {
  110. return;
  111. }
  112.  
  113. const css = `
  114. :root {
  115. color-scheme: dark;
  116. background-color: white;
  117. filter: invert(100%) hue-rotate(180deg) !important;
  118. -webkit-font-smoothing: antialiased;
  119. color: black;
  120. }
  121. body {
  122. color-scheme: light;
  123. background-color: #000 !important;
  124. background: linear-gradient(#fff, #fff);
  125. }
  126. img:not([src*="svg"]),video,iframe,embed,object,canvas,picture,.logo,.darkify-ignore {
  127. filter: invert(100%) hue-rotate(180deg) !important;
  128. }
  129. img[src*="svg"]:hover {
  130. background-color: #aaa !important;
  131. }
  132. `;
  133.  
  134.  
  135. function darkify() {
  136. const divs = document.querySelectorAll("div");
  137. for (const div of divs) {
  138. if (!div.classList.contains("darkify-ignore")) {
  139. continue;
  140. }
  141. const cs = getComputedStyle(div);
  142. const bg = cs.backgroundImage;
  143. const bs = cs.backgroundSize;
  144. if (bg === "none") {
  145. continue;
  146. }
  147. if (!["cover", "contain"].includes(bs)) {
  148. continue;
  149. }
  150. if (bg.includes(".svg") || bg.includes(".gif")) {
  151. continue;
  152. }
  153. div.classList.add("darkify-ignore");
  154. }
  155. if (document.getElementById("darkify")) {
  156. return;
  157. }
  158. const style = document.createElement("style");
  159. style.id = "darkify";
  160. style.innerHTML = css;
  161.  
  162. const themeColorMeta = document.createElement("meta");
  163. themeColorMeta.name = "theme-color";
  164. themeColorMeta.content = "#000000";
  165.  
  166. document.head.appendChild(style);
  167. document.head.appendChild(themeColorMeta);
  168. }
  169.  
  170. darkify();
  171.  
  172. document.addEventListener("DOMContentLoaded", darkify);
  173. })();