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.

目前为 2017-11-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Automatic Material Dark-Mode for YouTube
  3. // @namespace SteveJobzniak
  4. // @version 1.5.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. /* Performs multiple retries of a function call until it either succeeds or has failed all attempts. */
  22. function retryFnCall( fnCallback ) {
  23. // If we don't succeed immediately, we'll perform multiple retries.
  24. var success = fnCallback();
  25. if( ! success ) {
  26. var attempt = 0, maxAttempts = 40, waitDelay = 50; // 40 * 50ms = Max ~2 seconds of retries.
  27. var searchTimer = setInterval( function() {
  28. var success = fnCallback();
  29.  
  30. // If we've reached max attempts or found success, we must now stop the interval timer.
  31. if( ++attempt >= maxAttempts || success ) {
  32. clearInterval( searchTimer );
  33. }
  34. }, waitDelay );
  35. }
  36. }
  37.  
  38. /* Searches for a specific element. */
  39. function findElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback ) {
  40. var elems = parentElem.querySelectorAll( elemQuery );
  41. if( elems.length === expectedLength ) {
  42. var item = elems[selectItem];
  43. fnCallback( item );
  44. return true;
  45. }
  46.  
  47. //console.log('Debug: Cannot find "'+elemQuery+'".');
  48. return false;
  49. }
  50.  
  51. function retryFindElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback ) {
  52. // If we can't find the element immediately, we'll perform multiple retries.
  53. retryFnCall( function() {
  54. return findElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback );
  55. } );
  56. }
  57.  
  58. /* Searches for multiple different elements and uses the earliest match. */
  59. function multiFindElement( queryList, fnCallback ) {
  60. for( var i=0, len=queryList.length; i<len; ++i ) {
  61. var query = queryList[i];
  62. var success = findElement( query.parentElem, query.elemQuery, query.expectedLength, query.selectItem, fnCallback );
  63. if( success ) {
  64. // Don't try any other queries, since we've found a successful match.
  65. return true;
  66. }
  67. }
  68.  
  69. return false;
  70. }
  71.  
  72. function retryMultiFindElement( queryList, fnCallback ) {
  73. // If we can't find any of the elements immediately, we'll perform multiple retries.
  74. retryFnCall( function() {
  75. return multiFindElement( queryList, fnCallback );
  76. } );
  77. }
  78.  
  79. /* Automatically enables YouTube's dark mode theme. */
  80. function enableDark() {
  81. // Wait until the settings menu is available, to ensure that YouTube's "dark mode state" and code has been loaded...
  82. // Note that this particular menu button always exists (both when logged in and when logged out of your account),
  83. // but its actual icon and the list of submenu choices differ. However, its "dark mode" submenus are the same in either case.
  84. retryFindElement( document, 'button.style-scope.ytd-topbar-menu-button-renderer', 2, 1, function( settingsMenuButton ) {
  85. // Check the dark mode state "flag" and abort processing if dark mode is already active.
  86. if( document.documentElement.getAttribute( 'dark' ) === 'true' ) { return; }
  87.  
  88. // We MUST open the "settings" menu, otherwise nothing will react to the "toggle dark mode" event!
  89. settingsMenuButton.click();
  90.  
  91. // Wait a moment for the settings-menu to open up after clicking...
  92. retryFindElement( document, 'div#label.style-scope.ytd-toggle-theme-compact-link-renderer', 1, 0, function( darkModeSubMenuButton ) {
  93. // Next, go to the "toggle dark mode" settings sub-page.
  94. darkModeSubMenuButton.click();
  95.  
  96. // Wait a moment for the settings sub-page to switch...
  97. retryFindElement( document, 'ytd-toggle-item-renderer.style-scope.ytd-multi-page-menu-renderer', 1, 0, function( darkModeSubPageContainer ) {
  98. // Get a reference to the "activate dark mode" button...
  99. retryFindElement( darkModeSubPageContainer, 'paper-toggle-button.style-scope.ytd-toggle-item-renderer', 1, 0, function( darkModeButton ) {
  100. // We MUST now use this very ugly, hardcoded sleep-timer to ensure that YouTube's "activate dark mode" code is fully
  101. // loaded; otherwise, YouTube will be completely BUGGED OUT and WON'T save the fact that we've enabled dark mode!
  102. // Since JavaScript is single-threaded, this timeout simply ensures that we'll leave our current code so that we allow
  103. // YouTube's event handlers to deal with loading the settings-page, and then the timeout gives control back to us.
  104. setTimeout( function() {
  105. // Now simply click YouTube's button to enable their dark mode.
  106. darkModeButton.click();
  107.  
  108. // And lastly, give keyboard focus back to the input search field... (We don't need any setTimeout here...)
  109. retryFindElement( document, 'input#search', 1, 0, function( searchField ) {
  110. searchField.click(); // First, click the search-field to force the settings-panel to close...
  111. searchField.focus(); // ...and finally give the search-field focus! Voila!
  112. } );
  113. }, 20 ); // We can use 0ms here for "as soon as possible" instead, but our "at least 20ms" might be safer just in case.
  114. } );
  115. } );
  116. } );
  117. } );
  118.  
  119. // Alternative method, which switches using an internal YouTube event instead of clicking
  120. // the menus... I decided to disable this method, since it relies on intricate internal
  121. // details, and it still requires their menu to be open to work anyway (because their
  122. // code for changing theme isn't active until the Dark Mode settings menu is open),
  123. // so we may as well just click the actual menu items. ;-)
  124. /*
  125. var ytDebugMenu = document.querySelectorAll('ytd-debug-menu');
  126. ytDebugMenu = (ytDebugMenu.length === 1 ? ytDebugMenu[0] : undefined);
  127. if( ytDebugMenu ) {
  128. ytDebugMenu.fire(
  129. 'yt-action',
  130. {
  131. actionName:'yt-signal-action-toggle-dark-theme-on',
  132. optionalAction:false,
  133. args:[
  134. {signalAction:{signal:'TOGGLE_DARK_THEME_ON'}},
  135. toggleMenuElem,
  136. undefined
  137. ],
  138. returnValue: []
  139. },
  140. {}
  141. );
  142. }
  143. */
  144.  
  145. // Also note that it may be possible to simply modify the YouTube cookies, by changing
  146. // "PREF=f1=50000000;" to "PREF=f1=50000000&f6=400;" (dark mode on) and then reloading the page.
  147. // However, a reload is always slower than toggling the settings menu, so I didn't do that.
  148. }
  149.  
  150. if( document.readyState === 'complete' ) {
  151. enableDark();
  152. } else {
  153. document.addEventListener( 'readystatechange', function( evt ) {
  154. if( document.readyState === 'complete' ) {
  155. enableDark();
  156. }
  157. } );
  158. }
  159. })();