YouTube Auto Dark Mode

Automatically toggle built-in dark mode on youtube.com

  1. /* Copyright (C) 2020 Nathaniel Wu
  2. * Modified from ytAutoDark. Automatically toggle Youtube built-in dark theme.
  3. * Copyright (C) 2019-2020 Victor VOISIN
  4.  
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9.  
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14.  
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18.  
  19. // ==UserScript==
  20. // @name YouTube Auto Dark Mode
  21. // @namespace http://tampermonkey.net/
  22. // @version 3.0.3
  23. // @description Automatically toggle built-in dark mode on youtube.com
  24. // @author Victor VOISIN, Nathaniel Wu
  25. // @include *www.youtube.com/*
  26. // @license GPL-3.0-or-later
  27. // @grant none
  28. // ==/UserScript==
  29.  
  30. (function () {
  31. 'use strict';
  32. /**
  33. * Is dark theme enabled ?
  34. */
  35. const isDarkThemeEnabled = () => {
  36. return Boolean(document.querySelector('html').hasAttribute('dark'));
  37. };
  38.  
  39. /**
  40. * Three dot menu button.
  41. */
  42. const isMenuButtonAvailableInDom = () => {
  43. return Boolean(
  44. document.querySelectorAll('ytd-topbar-menu-button-renderer')[2],
  45. );
  46. };
  47.  
  48. const clickMenu = () => {
  49. document.querySelectorAll('ytd-topbar-menu-button-renderer')[2].click();
  50. };
  51.  
  52. const isMenuOpen = () => {
  53. return (
  54. document.querySelector('iron-dropdown') &&
  55. !document.querySelector('iron-dropdown').getAttribute('aria-hidden')
  56. );
  57. };
  58.  
  59. const isMenuLoading = () => {
  60. return !document.getElementById('spinner');
  61. };
  62.  
  63. /**
  64. * Link arrow to dark theme popup.
  65. */
  66. const isCompactLinkAvailableInDom = () => {
  67. return Boolean(
  68. document.querySelector('ytd-toggle-theme-compact-link-renderer'),
  69. );
  70. };
  71.  
  72. const clickRenderer = () => {
  73. document.querySelector('ytd-toggle-theme-compact-link-renderer').click();
  74. };
  75.  
  76. const isRendererOpen = () => {
  77. return !(
  78. document.getElementById('submenu') &&
  79. Boolean(document.getElementById('submenu').hasAttribute('hidden'))
  80. );
  81. };
  82.  
  83. const isRendererLoading = () => {
  84. return !(
  85. document.querySelector('#spinner.ytd-multi-page-menu-renderer') &&
  86. document
  87. .querySelector('#spinner.ytd-multi-page-menu-renderer')
  88. .hasAttribute('hidden')
  89. );
  90. };
  91.  
  92. /**
  93. * Check theme menu.
  94. */
  95. const ThemeMenuType = {
  96. "none": 0,
  97. "toggle": 1,
  98. "menu": 2
  99. }
  100. const isThemeMenuAvailableInDom = () => {
  101. let ret = ThemeMenuType.none;
  102. if (Boolean(document.querySelector('#caption-container > paper-toggle-button')))
  103. ret = ThemeMenuType.toggle;
  104. else if (Boolean(document.querySelector('ytd-multi-page-menu-renderer > #submenu #container #sections #items > ytd-compact-link-renderer')))
  105. ret = ThemeMenuType.menu;
  106. return ret;
  107. };
  108.  
  109. /**
  110. * Toggle dark theme by clicking element in DOM.
  111. */
  112. const toggleDarkTheme = () => {
  113. let themeMenuType;
  114. if (isCompactLinkAvailableInDom() && (themeMenuType = isThemeMenuAvailableInDom())) {
  115. switch (themeMenuType) {
  116. case ThemeMenuType.toggle: {
  117. document
  118. .querySelector('#caption-container > paper-toggle-button')
  119. .click();
  120. break;
  121. }
  122. case ThemeMenuType.menu: {
  123. document
  124. .querySelector(`ytd-multi-page-menu-renderer > #submenu #container #sections #items > ytd-compact-link-renderer:nth-of-type(${isDarkThemeEnabled() ? 4 : 3})`)
  125. .click();
  126. break;
  127. }
  128. default: {
  129. console.log('Unknown theme menu type');
  130. }
  131. }
  132. } else {
  133. setTimeout(() => {
  134. window.requestAnimationFrame(tryTogglingDarkMode);
  135. }, 50);
  136. }
  137. };
  138.  
  139. /**
  140. * Wait for all elements to exist in DOM then toggle
  141. * Step 1: Wait for 3 dots menu in DOM.
  142. * Step 2: Click on 3 dots to open menu.
  143. * Step 3: Wait for menu to finish loading.
  144. * Step 4: Waiting for link to sub-menu (Should be optional now, because of step 3).
  145. * Step 5: Click to open sub-menu (renderer pane).
  146. * Step 6: Wait for sub-menu to finish loading.
  147. * Step 7: Toggle dark theme.
  148. * Step 8: Close menu.
  149. */
  150. let start = null;
  151. const tryTogglingDarkMode = timestamp => {
  152. // Compute runtime
  153. if (!start) {
  154. start = timestamp;
  155. }
  156. const runtime = timestamp - start;
  157. // Try to toggle only during 10s
  158. if (runtime < 10000) {
  159. if (!isMenuButtonAvailableInDom()) {
  160. setTimeout(() => {
  161. window.requestAnimationFrame(tryTogglingDarkMode);
  162. }, 50);
  163. } else if (!isMenuOpen()) {
  164. clickMenu();
  165. setTimeout(() => {
  166. window.requestAnimationFrame(tryTogglingDarkMode);
  167. }, 50);
  168. } else if (isMenuLoading()) {
  169. setTimeout(() => {
  170. window.requestAnimationFrame(tryTogglingDarkMode);
  171. }, 50);
  172. } else if (isMenuOpen() && !isCompactLinkAvailableInDom()) {
  173. setTimeout(() => {
  174. window.requestAnimationFrame(tryTogglingDarkMode);
  175. }, 50);
  176. } else if (!isRendererOpen()) {
  177. clickRenderer();
  178. setTimeout(() => {
  179. window.requestAnimationFrame(tryTogglingDarkMode);
  180. }, 50);
  181. } else if (isRendererOpen() && isRendererLoading()) {
  182. setTimeout(() => {
  183. window.requestAnimationFrame(tryTogglingDarkMode);
  184. }, 50);
  185. } else {
  186. toggleDarkTheme();
  187. // clickRenderer(); // Close dark theme menu
  188. if (isMenuOpen()) {
  189. clickMenu();
  190. }
  191. }
  192. } else {
  193. // Timeout with new activation process. Try the old one.
  194. setTimeout(() => {
  195. window.requestAnimationFrame(tryTogglingDarkModeTheOldWay);
  196. }, 50);
  197. }
  198. };
  199.  
  200. /**
  201. * @Deprecated
  202. * Old way of doing things.
  203. * Kept here for backward compatibility.
  204. * Will be removed in a few month.
  205. */
  206.  
  207. /**
  208. * @Deprecated
  209. */
  210. const openCloseMenu = () => {
  211. document.querySelectorAll('ytd-topbar-menu-button-renderer')[2].click();
  212. document.querySelectorAll('ytd-topbar-menu-button-renderer')[2].click();
  213. };
  214.  
  215. /**
  216. * @Deprecated
  217. */
  218. const openCloseRenderer = () => {
  219. document.querySelector('ytd-toggle-theme-compact-link-renderer').click();
  220. document.querySelector('ytd-toggle-theme-compact-link-renderer').click();
  221. };
  222.  
  223. /**
  224. * @Deprecated
  225. */
  226. let startOldWay = null;
  227. const tryTogglingDarkModeTheOldWay = timestamp => {
  228. // Compute runtime
  229. if (!startOldWay) {
  230. startOldWay = timestamp;
  231. }
  232. const runtime = timestamp - startOldWay;
  233. // Try to toggle only during 5s
  234. if (runtime < 5000) {
  235. if (!isMenuButtonAvailableInDom()) {
  236. window.requestAnimationFrame(tryTogglingDarkMode);
  237. } else if (!isCompactLinkAvailableInDom()) {
  238. openCloseMenu();
  239. window.requestAnimationFrame(tryTogglingDarkMode);
  240. } else if (!isThemeMenuAvailableInDom()) {
  241. openCloseRenderer();
  242. window.requestAnimationFrame(tryTogglingDarkMode);
  243. } else {
  244. toggleDarkTheme();
  245. startOldWay = null;
  246. }
  247. }
  248. };
  249.  
  250. const setDarkMode = on => {
  251. const isDarkModeOn = isDarkThemeEnabled();
  252. if (on) {
  253. if (!isDarkModeOn) {
  254. window.requestAnimationFrame(tryTogglingDarkMode);
  255. }
  256. } else if (isDarkModeOn) {
  257. window.requestAnimationFrame(tryTogglingDarkMode);
  258. }
  259. };
  260.  
  261. const inIframe = () => {
  262. try {
  263. return window.self !== window.top;
  264. } catch (e) {
  265. return true;
  266. }
  267. }
  268.  
  269. /**
  270. * Execute
  271. */
  272. if (inIframe())
  273. return;
  274. if (window.matchMedia) {// if the browser/os supports system-level color scheme
  275. setDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
  276. window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => setDarkMode(e.matches));
  277. } else {// otherwise use local time to decide
  278. let hour = (new Date()).getHours();
  279. setDarkMode(hour > 18 || hour < 8);
  280. }
  281. })();