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-09-21 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Automatic Material Dark-Mode for YouTube
// @namespace    SteveJobzniak
// @version      1.4
// @description  A low-tech solution to a high-tech problem! Automatically clicks YouTube's "Dark Mode" button if dark mode isn't already active.
// @author       SteveJobzniak
// @match        *://www.youtube.com/*
// @exclude      *://www.youtube.com/tv*
// @exclude      *://www.youtube.com/embed/*
// @run-at       document-end
// @grant        none
// @noframes
// ==/UserScript==

(function() {
    'use strict';

    /* Performs multiple retries of a function call until it either succeeds or has failed all attempts. */
    function retryFnCall( fnCallback ) {
        // If we don't succeed immediately, we'll perform multiple retries.
        var success = fnCallback();
        if( ! success ) {
            var attempt = 0, maxAttempts = 40, waitDelay = 50; // 40 * 50ms = Max ~2 seconds of retries.
            var searchTimer = setInterval( function() {
                var success = fnCallback();

                // If we've reached max attempts or found success, we must now stop the interval timer.
                if( ++attempt >= maxAttempts || success ) {
                    clearInterval( searchTimer );
                }
            }, waitDelay );
        }
    }

    /* Searches for a specific element. */
    function findElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback ) {
        var elems = parentElem.querySelectorAll( elemQuery );
        if( elems.length === expectedLength ) {
            var item = elems[selectItem];
            fnCallback( item );
            return true;
        }

        //console.log('Debug: Cannot find "'+elemQuery+'".');
        return false;
    }

    function retryFindElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback ) {
        // If we can't find the element immediately, we'll perform multiple retries.
        retryFnCall( function() {
            return findElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback );
        } );
    }

    /* Searches for multiple different elements and uses the earliest match. */
    function multiFindElement( queryList, fnCallback ) {
        for( var i=0, len=queryList.length; i<len; ++i ) {
            var query = queryList[i];
            var success = findElement( query.parentElem, query.elemQuery, query.expectedLength, query.selectItem, fnCallback );
            if( success ) {
                // Don't try any other queries, since we've found a successful match.
                return true;
            }
        }

        return false;
    }

    function retryMultiFindElement( queryList, fnCallback ) {
        // If we can't find any of the elements immediately, we'll perform multiple retries.
        retryFnCall( function() {
            return multiFindElement( queryList, fnCallback );
        } );
    }

    /* Automatically enables YouTube's dark mode theme. */
    function enableDark() {
        // Wait until the settings menu is available, to ensure that YouTube's "dark mode state" and code has been loaded...
        // Note that this particular menu button always exists (both when logged in and when logged out of your account),
        // but its actual icon and the list of submenu choices differ. However, its "dark mode" submenus are the same in either case.
        retryFindElement( document, 'button.style-scope.ytd-topbar-menu-button-renderer', 2, 1, function( settingsMenuButton ) {
            // Check the dark mode state "flag" and abort processing if dark mode is already active.
            if( document.body.getAttribute( 'dark' ) === 'true' ) { return; }

            // We MUST open the "settings" menu, otherwise nothing will react to the "toggle dark mode" event!
            settingsMenuButton.click();

            // Wait a moment for the settings-menu to open up after clicking...
            retryFindElement( document, 'div#label.style-scope.ytd-toggle-theme-compact-link-renderer', 1, 0, function( darkModeSubMenuButton ) {
                // Next, go to the "toggle dark mode" settings sub-page.
                darkModeSubMenuButton.click();

                // Wait a moment for the settings sub-page to switch...
                retryFindElement( document, 'ytd-toggle-item-renderer.style-scope.ytd-multi-page-menu-renderer', 1, 0, function( darkModeSubPageContainer ) {
                    // Get a reference to the "activate dark mode" button...
                    retryFindElement( darkModeSubPageContainer, 'paper-toggle-button.style-scope.ytd-toggle-item-renderer', 1, 0, function( darkModeButton ) {
                        // We MUST now use this very ugly, hardcoded sleep-timer to ensure that YouTube's "activate dark mode" code is fully
                        // loaded; otherwise, YouTube will be completely BUGGED OUT and WON'T save the fact that we've enabled dark mode!
                        // Since JavaScript is single-threaded, this timeout simply ensures that we'll leave our current code so that we allow
                        // YouTube's event handlers to deal with loading the settings-page, and then the timeout gives control back to us.
                        setTimeout( function() {
                            // Now simply click YouTube's button to enable their dark mode.
                            darkModeButton.click();

                            // And lastly, give keyboard focus back to the input search field... (We don't need any setTimeout here...)
                            retryFindElement( document, 'input#search', 1, 0, function( searchField ) {
                                searchField.click(); // First, click the search-field to force the settings-panel to close...
                                searchField.focus(); // ...and finally give the search-field focus! Voila!
                            } );
                        }, 20 ); // We can use 0ms here for "as soon as possible" instead, but our "at least 20ms" might be safer just in case.
                    } );
                } );
            } );
        } );

        // Alternative method, which switches using an internal YouTube event instead of clicking
        // the menus... I decided to disable this method, since it relies on intricate internal
        // details, and it still requires their menu to be open to work anyway (because their
        // code for changing theme isn't active until the Dark Mode settings menu is open),
        // so we may as well just click the actual menu items. ;-)
        /*
        var ytDebugMenu = document.querySelectorAll('ytd-debug-menu');
        ytDebugMenu = (ytDebugMenu.length === 1 ? ytDebugMenu[0] : undefined);
        if( ytDebugMenu ) {
            ytDebugMenu.fire(
                'yt-action',
                {
                    actionName:'yt-signal-action-toggle-dark-theme-on',
                    optionalAction:false,
                    args:[
                        {signalAction:{signal:'TOGGLE_DARK_THEME_ON'}},
                        toggleMenuElem,
                        undefined
                    ],
                    returnValue: []
                },
                {}
            );
        }
        */

        // Also note that it may be possible to simply modify the YouTube cookies, by changing
        // "PREF=f1=50000000;" to "PREF=f1=50000000&f6=400;" (dark mode on) and then reloading the page.
        // However, a reload is always slower than toggling the settings menu, so I didn't do that.
    }

    if( document.readyState === 'complete' ) {
        enableDark();
    } else {
        document.addEventListener( 'readystatechange', function( evt ) {
            if( document.readyState === 'complete' ) {
                enableDark();
            }
        } );
    }
})();