YouTube Auto Dark Mode

Automatically toggle built-in dark mode on youtube.com

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

/* Copyright (C) 2020  Nathaniel Wu
 * Modified from ytAutoDark. Automatically toggle Youtube built-in dark theme.
 * Copyright (C) 2019-2020  Victor VOISIN

 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

// ==UserScript==
// @name         YouTube Auto Dark Mode
// @namespace    http://tampermonkey.net/
// @version      3.0.3
// @description  Automatically toggle built-in dark mode on youtube.com
// @author       Victor VOISIN, Nathaniel Wu
// @include      *www.youtube.com/*
// @license      GPL-3.0-or-later
// @grant        none
// ==/UserScript==

(function () {
    'use strict';
    /**
     * Is dark theme enabled ?
     */
    const isDarkThemeEnabled = () => {
        return Boolean(document.querySelector('html').hasAttribute('dark'));
    };

    /**
     * Three dot menu button.
     */
    const isMenuButtonAvailableInDom = () => {
        return Boolean(
            document.querySelectorAll('ytd-topbar-menu-button-renderer')[2],
        );
    };

    const clickMenu = () => {
        document.querySelectorAll('ytd-topbar-menu-button-renderer')[2].click();
    };

    const isMenuOpen = () => {
        return (
            document.querySelector('iron-dropdown') &&
            !document.querySelector('iron-dropdown').getAttribute('aria-hidden')
        );
    };

    const isMenuLoading = () => {
        return !document.getElementById('spinner');
    };

    /**
     * Link arrow to dark theme popup.
     */
    const isCompactLinkAvailableInDom = () => {
        return Boolean(
            document.querySelector('ytd-toggle-theme-compact-link-renderer'),
        );
    };

    const clickRenderer = () => {
        document.querySelector('ytd-toggle-theme-compact-link-renderer').click();
    };

    const isRendererOpen = () => {
        return !(
            document.getElementById('submenu') &&
            Boolean(document.getElementById('submenu').hasAttribute('hidden'))
        );
    };

    const isRendererLoading = () => {
        return !(
            document.querySelector('#spinner.ytd-multi-page-menu-renderer') &&
            document
                .querySelector('#spinner.ytd-multi-page-menu-renderer')
                .hasAttribute('hidden')
        );
    };

    /**
     * Check theme menu.
     */
    const ThemeMenuType = {
        "none": 0,
        "toggle": 1,
        "menu": 2
    }
    const isThemeMenuAvailableInDom = () => {
        let ret = ThemeMenuType.none;
        if (Boolean(document.querySelector('#caption-container > paper-toggle-button')))
            ret = ThemeMenuType.toggle;
        else if (Boolean(document.querySelector('ytd-multi-page-menu-renderer > #submenu #container #sections #items > ytd-compact-link-renderer')))
            ret = ThemeMenuType.menu;
        return ret;
    };

    /**
     * Toggle dark theme by clicking element in DOM.
     */
    const toggleDarkTheme = () => {
        let themeMenuType;
        if (isCompactLinkAvailableInDom() && (themeMenuType = isThemeMenuAvailableInDom())) {
            switch (themeMenuType) {
                case ThemeMenuType.toggle: {
                    document
                        .querySelector('#caption-container > paper-toggle-button')
                        .click();
                    break;
                }
                case ThemeMenuType.menu: {
                    document
                        .querySelector(`ytd-multi-page-menu-renderer > #submenu #container #sections #items > ytd-compact-link-renderer:nth-of-type(${isDarkThemeEnabled() ? 4 : 3})`)
                        .click();
                    break;
                }
                default: {
                    console.log('Unknown theme menu type');
                }
            }
        } else {
            setTimeout(() => {
                window.requestAnimationFrame(tryTogglingDarkMode);
            }, 50);
        }
    };

    /**
     * Wait for all elements to exist in DOM then toggle
     * Step 1: Wait for 3 dots menu in DOM.
     * Step 2: Click on 3 dots to open menu.
     * Step 3: Wait for menu to finish loading.
     * Step 4: Waiting for link to sub-menu (Should be optional now, because of step 3).
     * Step 5: Click to open sub-menu (renderer pane).
     * Step 6: Wait for sub-menu to finish loading.
     * Step 7: Toggle dark theme.
     * Step 8: Close menu.
     */
    let start = null;
    const tryTogglingDarkMode = timestamp => {
        // Compute runtime
        if (!start) {
            start = timestamp;
        }
        const runtime = timestamp - start;
        // Try to toggle only during 10s
        if (runtime < 10000) {
            if (!isMenuButtonAvailableInDom()) {
                setTimeout(() => {
                    window.requestAnimationFrame(tryTogglingDarkMode);
                }, 50);
            } else if (!isMenuOpen()) {
                clickMenu();
                setTimeout(() => {
                    window.requestAnimationFrame(tryTogglingDarkMode);
                }, 50);
            } else if (isMenuLoading()) {
                setTimeout(() => {
                    window.requestAnimationFrame(tryTogglingDarkMode);
                }, 50);
            } else if (isMenuOpen() && !isCompactLinkAvailableInDom()) {
                setTimeout(() => {
                    window.requestAnimationFrame(tryTogglingDarkMode);
                }, 50);
            } else if (!isRendererOpen()) {
                clickRenderer();
                setTimeout(() => {
                    window.requestAnimationFrame(tryTogglingDarkMode);
                }, 50);
            } else if (isRendererOpen() && isRendererLoading()) {
                setTimeout(() => {
                    window.requestAnimationFrame(tryTogglingDarkMode);
                }, 50);
            } else {
                toggleDarkTheme();
                // clickRenderer(); // Close dark theme menu
                if (isMenuOpen()) {
                    clickMenu();
                }
            }
        } else {
            // Timeout with new activation process. Try the old one.
            setTimeout(() => {
                window.requestAnimationFrame(tryTogglingDarkModeTheOldWay);
            }, 50);
        }
    };

    /**
     * @Deprecated
     * Old way of doing things.
     * Kept here for backward compatibility.
     * Will be removed in a few month.
     */

    /**
     * @Deprecated
     */
    const openCloseMenu = () => {
        document.querySelectorAll('ytd-topbar-menu-button-renderer')[2].click();
        document.querySelectorAll('ytd-topbar-menu-button-renderer')[2].click();
    };

    /**
     * @Deprecated
     */
    const openCloseRenderer = () => {
        document.querySelector('ytd-toggle-theme-compact-link-renderer').click();
        document.querySelector('ytd-toggle-theme-compact-link-renderer').click();
    };

    /**
     * @Deprecated
     */
    let startOldWay = null;
    const tryTogglingDarkModeTheOldWay = timestamp => {
        // Compute runtime
        if (!startOldWay) {
            startOldWay = timestamp;
        }
        const runtime = timestamp - startOldWay;
        // Try to toggle only during 5s
        if (runtime < 5000) {
            if (!isMenuButtonAvailableInDom()) {
                window.requestAnimationFrame(tryTogglingDarkMode);
            } else if (!isCompactLinkAvailableInDom()) {
                openCloseMenu();
                window.requestAnimationFrame(tryTogglingDarkMode);
            } else if (!isThemeMenuAvailableInDom()) {
                openCloseRenderer();
                window.requestAnimationFrame(tryTogglingDarkMode);
            } else {
                toggleDarkTheme();
                startOldWay = null;
            }
        }
    };

    const setDarkMode = on => {
        const isDarkModeOn = isDarkThemeEnabled();
        if (on) {
            if (!isDarkModeOn) {
                window.requestAnimationFrame(tryTogglingDarkMode);
            }
        } else if (isDarkModeOn) {
            window.requestAnimationFrame(tryTogglingDarkMode);
        }
    };

    const inIframe = () => {
        try {
            return window.self !== window.top;
        } catch (e) {
            return true;
        }
    }

    /**
     * Execute
     */
    if (inIframe())
        return;
    if (window.matchMedia) {// if the browser/os supports system-level color scheme
        setDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => setDarkMode(e.matches));
    } else {// otherwise use local time to decide
        let hour = (new Date()).getHours();
        setDarkMode(hour > 18 || hour < 8);
    }
})();