Greasy Fork is available in English.

Automatic Material Dark-Mode for YouTube

A low-tech solution to a high-tech problem! Automatically clicks YouTube's "Dark Mode" button if dark mode isn't already active.

  1. // ==UserScript==
  2. // @name Automatic Material Dark-Mode for YouTube
  3. // @namespace SteveJobzniak
  4. // @version 1.7.0
  5. // @description A low-tech solution to a high-tech problem! Automatically clicks YouTube's "Dark Mode" button if dark mode isn't already active.
  6. // @author SteveJobzniak
  7. // @homepage https://greasyfork.org/scripts/32954-automatic-material-dark-mode-for-youtube
  8. // @license https://www.apache.org/licenses/LICENSE-2.0
  9. // @contributionURL https://www.paypal.me/Armindale/0usd
  10. // @match *://www.youtube.com/*
  11. // @exclude *://www.youtube.com/tv*
  12. // @exclude *://www.youtube.com/embed/*
  13. // @run-at document-end
  14. // @grant none
  15. // @noframes
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. /* --- START: Utils-MultiRetry v1.1 by SteveJobzniak --- */
  22.  
  23. /* Performs multiple retries of a function call until it either succeeds or has failed all attempts. */
  24. var retryFnCall = function(fnCallback, maxAttempts, waitDelay) {
  25. // Default parameters: 40 * 50ms = Max ~2 seconds of additional retries.
  26. maxAttempts = (typeof maxAttempts !== 'undefined') ? maxAttempts : 40;
  27. waitDelay = (typeof waitDelay !== 'undefined') ? waitDelay : 50;
  28.  
  29. // If we don't succeed immediately, we'll perform multiple retries.
  30. var success = fnCallback();
  31. if (!success) {
  32. var attempt = 0;
  33. var searchTimer = setInterval(function() {
  34. var success = fnCallback();
  35.  
  36. // If we've reached max attempts or found success, we must now stop the interval timer.
  37. if (++attempt >= maxAttempts || success) {
  38. clearInterval(searchTimer);
  39. }
  40. }, waitDelay);
  41. }
  42. };
  43.  
  44. /* --- END: Utils-MultiRetry by SteveJobzniak --- */
  45.  
  46. /* --- START: Utils-ElementFinder v1.3 by SteveJobzniak --- */
  47.  
  48. /* Searches for a specific element. */
  49. var findElement = function(parentElem, elemQuery, expectedLength, selectItem, fnCallback) {
  50. var elems = parentElem.querySelectorAll(elemQuery);
  51. if (elems.length === expectedLength) {
  52. var item = elems[selectItem];
  53. fnCallback(item);
  54. return true;
  55. }
  56.  
  57. //console.log('Debug: Cannot find "'+elemQuery+'".');
  58. return false;
  59. };
  60.  
  61. var retryFindElement = function(parentElem, elemQuery, expectedLength, selectItem, fnCallback, maxAttempts, waitDelay) {
  62. // If we can't find the element immediately, we'll perform multiple retries.
  63. retryFnCall(function() {
  64. return findElement(parentElem, elemQuery, expectedLength, selectItem, fnCallback);
  65. }, maxAttempts, waitDelay);
  66. };
  67.  
  68. /* Searches for multiple different elements and uses the earliest match. */
  69. var multiFindElement = function(queryList, fnCallback) {
  70. for (var i = 0, len = queryList.length; i < len; ++i) {
  71. var query = queryList[i];
  72. var success = findElement(query.parentElem, query.elemQuery, query.expectedLength, query.selectItem, fnCallback);
  73. if (success) {
  74. // Don't try any other queries, since we've found a successful match.
  75. return true;
  76. }
  77. }
  78.  
  79. return false;
  80. };
  81.  
  82. var retryMultiFindElement = function(queryList, fnCallback, maxAttempts, waitDelay) {
  83. // If we can't find any of the elements immediately, we'll perform multiple retries.
  84. retryFnCall(function() {
  85. return multiFindElement(queryList, fnCallback);
  86. }, maxAttempts, waitDelay);
  87. };
  88.  
  89. /* --- END: Utils-ElementFinder by SteveJobzniak --- */
  90.  
  91. /* Automatically enables YouTube's dark mode theme. */
  92. var enableDark = function() {
  93. // Refuse to proceed if the user is on the old non-Material YouTube theme (which has no dark mode).
  94. // NOTE: This is just to avoid getting "error reports" by people who aren't even on YouTube's new theme.
  95. var oldYouTube = document.getElementById('body-container');
  96. if (oldYouTube && document.body.id === 'body') {
  97. var errorMessage = 'You are using the old YouTube theme. Enable the new theme <a href="https://youtube.com/new" style="color:#fff;text-decoration:underline">here</a>.';
  98. var errorDiv = document.createElement('div');
  99. errorDiv.style.position = 'fixed';
  100. errorDiv.style.bottom = 0;
  101. errorDiv.style.left = 0;
  102. errorDiv.style.width = '100%';
  103. errorDiv.style.padding = '10px';
  104. errorDiv.style.textAlign = 'center';
  105. errorDiv.style.fontSize = '130%';
  106. errorDiv.style.fontWeight = 'bold';
  107. errorDiv.style.color = '#fff';
  108. errorDiv.style.backgroundColor = 'rgba(244, 67, 54, 0.9)';
  109. errorDiv.style.zIndex = '99999';
  110. errorDiv.innerHTML = errorMessage;
  111. document.body.appendChild(errorDiv);
  112.  
  113. return;
  114. }
  115.  
  116. // Wait until the settings menu is available, to ensure that YouTube's "dark mode state" and code has been loaded...
  117. // Note that this particular menu button always exists (both when logged in and when logged out of your account),
  118. // but its actual icon and the list of submenu choices differ. However, its "dark mode" submenus are the same in either case.
  119. retryFnCall(function() {
  120. // The menu button count varies based on the browser. We expect to find either 2 or 3 buttons, and the settings menu
  121. // is always the last button (even when logged in). Sadly there is no better way to find the correct button,
  122. // since YouTube doesn't have any identifiable language-agnostic labels or icons in the HTML. Sigh...
  123. var buttons = document.querySelectorAll('ytd-topbar-menu-button-renderer button');
  124. if (buttons.length !== 2 && buttons.length !== 3) {
  125. return false; // Failed to find any of the expected menu button counts. Retry...
  126. }
  127. var settingsMenuButton = buttons[buttons.length - 1];
  128.  
  129. // Check the dark mode state "flag" and abort processing if dark mode is already active.
  130. if (document.documentElement.getAttribute('dark') === 'true') {
  131. return true; // Stop retrying...
  132. }
  133.  
  134. // We MUST open the "settings" menu, otherwise nothing will react to the "toggle dark mode" event!
  135. settingsMenuButton.click();
  136.  
  137. // Wait a moment for the settings-menu to open up after clicking...
  138. retryFindElement(document, 'div#label.style-scope.ytd-toggle-theme-compact-link-renderer', 1, 0, function(darkModeSubMenuButton) {
  139. // Next, go to the "toggle dark mode" settings sub-page.
  140. darkModeSubMenuButton.click();
  141.  
  142. // Wait a moment for the settings sub-page to switch...
  143. retryFindElement(document, 'ytd-toggle-item-renderer.style-scope.ytd-multi-page-menu-renderer', 1, 0, function(darkModeSubPageContainer) {
  144. // Get a reference to the "activate dark mode" button...
  145. retryFindElement(darkModeSubPageContainer, 'paper-toggle-button.style-scope.ytd-toggle-item-renderer', 1, 0, function(darkModeButton) {
  146. // We MUST now use this very ugly, hardcoded sleep-timer to ensure that YouTube's "activate dark mode" code is fully
  147. // loaded; otherwise, YouTube will be completely BUGGED OUT and WON'T save the fact that we've enabled dark mode!
  148. // Since JavaScript is single-threaded, this timeout simply ensures that we'll leave our current code so that we allow
  149. // YouTube's event handlers to deal with loading the settings-page, and then the timeout gives control back to us.
  150. setTimeout(function() {
  151. // Now simply click YouTube's button to enable their dark mode.
  152. darkModeButton.click();
  153.  
  154. // And lastly, give keyboard focus back to the input search field... (We don't need any setTimeout here...)
  155. retryFindElement(document, 'input#search', 1, 0, function(searchField) {
  156. searchField.click(); // First, click the search-field to force the settings-panel to close...
  157. searchField.focus(); // ...and finally give the search-field focus! Voila!
  158. });
  159. }, 30); // We can use 0ms here for "as soon as possible" instead, but our "at least 30ms" might be safer just in case.
  160. });
  161. });
  162. });
  163.  
  164. return true; // Stop retrying, since we've found and clicked the menu...
  165. }, 120, 50); // 120 * 50ms = ~6 seconds of retries.
  166.  
  167. // Alternative method, which switches using an internal YouTube event instead of clicking
  168. // the menus... I decided to disable this method, since it relies on intricate internal
  169. // details, and it still requires their menu to be open to work anyway (because their
  170. // code for changing theme isn't active until the Dark Mode settings menu is open),
  171. // so we may as well just click the actual menu items. ;-)
  172. /*
  173. var ytDebugMenu = document.querySelectorAll('ytd-debug-menu');
  174. ytDebugMenu = (ytDebugMenu.length === 1 ? ytDebugMenu[0] : undefined);
  175. if( ytDebugMenu ) {
  176. ytDebugMenu.fire(
  177. 'yt-action',
  178. {
  179. actionName:'yt-signal-action-toggle-dark-theme-on',
  180. optionalAction:false,
  181. args:[
  182. {signalAction:{signal:'TOGGLE_DARK_THEME_ON'}},
  183. toggleMenuElem,
  184. undefined
  185. ],
  186. returnValue: []
  187. },
  188. {}
  189. );
  190. }
  191. */
  192.  
  193. // Also note that it may be possible to simply modify the YouTube cookies, by changing
  194. // "PREF=f1=50000000;" to "PREF=f1=50000000&f6=400;" (dark mode on) and then reloading the page.
  195. // However, a reload is always slower than toggling the settings menu, so I didn't do that.
  196. };
  197.  
  198. if (document.readyState === 'complete') {
  199. enableDark();
  200. } else {
  201. document.addEventListener('readystatechange', function(evt) {
  202. if (document.readyState === 'complete') {
  203. enableDark();
  204. }
  205. });
  206. }
  207. })();