Twitter/X Auto Dark Theme Switch

Enhanced theme management for Twitter/X with native support detection and fallback

// ==UserScript==
// @name         Twitter/X Auto Dark Theme Switch
// @version      0.1.7
// @description  Enhanced theme management for Twitter/X with native support detection and fallback
// @icon         https://raw.githubusercontent.com/TiancongLx/Tempermonkey/refs/heads/main/twitter_auto_dark_theme_switch/icon_X.png
// @license      WTFPL
// @author       Original: 13xforever, Enhanced: tianconglx
// @match        https://twitter.com/*
// @match        https://x.com/*
// @run-at       document-start
// @namespace    https://github.com/TiancongLx
// ==/UserScript==


(function() {
    "use strict";

    /**
     * Twitter/X 主题管理器
     * - 检测原生自动主题功能
     * - 在没有原生支持时提供自动主题切换
     * - 支持优雅降级和渐进增强
     */
    class XThemeManager {
        constructor() {
            // 配置常量
            this.CONFIG = {
                THEMES: {
                    LIGHT: "0",  // X 的亮色主题值
                    DIM:   "1",  // X 的暗色主题值
                    DARK:  "2",  // X 的深色主题值(备用)
                },
                COOKIE: {
                    NAME: "night_mode",
                    DOMAINS: ['.twitter.com', '.x.com'],
                    MAX_AGE: 365 * 24 * 60 * 60  // 一年的秒数
                },
                DETECTION: {
                    INTERVAL: 2000,  // 检测间隔(毫秒)
                    MAX_ATTEMPTS: 5  // 最大检测次数
                },
                DEBUG: false  // 是否启用调试日志
            };

            // 内部状态
            this.state = {
                enabled: false,
                nativeSupport: false,
                mediaQuery: null,
                eventListeners: new Set()
            };
        }

        /**
         * 初始化主题管理器
         * @returns {Promise<void>}
         */
        async initialize() {
            this.log('Initializing theme manager...');

            try {
                // 检查浏览器特性支持
                if (!this.checkBrowserSupport()) {
                    this.log('Browser features not supported', 'warn');
                    return;
                }

                // 检查是否存在原生自动主题功能
                const hasNativeSupport = await this.checkNativeAutoTheme();

                if (hasNativeSupport) {
                    this.log('Native auto theme detected, script disabled');
                    this.state.nativeSupport = true;
                    return;
                }

                // 如果没有原生支持,启用脚本功能
                this.enableScriptTheme();
                this.log('Script theme management enabled');
            } catch (error) {
                this.log(`Initialization failed: ${error.message}`, 'error');
                throw error;
            }
        }

        /**
         * 检查浏览器必要特性支持
         * @returns {boolean}
         */
        checkBrowserSupport() {
            return window.matchMedia &&
                   document.cookie !== undefined &&
                   window.localStorage;
        }

        /**
         * 检测原生自动主题功能
         * @returns {Promise<boolean>}
         */
        async checkNativeAutoTheme() {
            let checksRemaining = this.CONFIG.DETECTION.MAX_ATTEMPTS;

            const checkFeatures = () => {
                return new Promise(resolve => {
                    const check = () => {
                        // 1. 检查设置面板中的自动主题选项
                        const hasToggle = !!document.querySelector('[data-testid="auto-theme-switch"]');

                        // 2. 检查主题相关的 CSS 变量
                        const root = document.documentElement;
                        const hasCSS = window.getComputedStyle(root)
                            .getPropertyValue('--auto-theme-enabled') === 'true';

                        // 3. 检查可能存在的主题 API
                        const hasAPI = window.__X_THEME_API__?.autoThemeSupported;

                        if (hasToggle || hasCSS || hasAPI) {
                            resolve(true);
                            return;
                        }

                        checksRemaining--;
                        if (checksRemaining > 0) {
                            setTimeout(check, this.CONFIG.DETECTION.INTERVAL);
                        } else {
                            resolve(false);
                        }
                    };

                    check();
                });
            };

            return await checkFeatures();
        }

        /**
         * 启用脚本的主题管理功能
         */
        enableScriptTheme() {
            // 设置媒体查询
            this.state.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

            // 创建主题变化处理函数
            const handleThemeChange = (e) => {
                const theme = e.matches ? this.CONFIG.THEMES.DIM : this.CONFIG.THEMES.LIGHT;
                this.setThemeCookie(theme);
            };

            // 初始设置
            handleThemeChange(this.state.mediaQuery);

            // 添加事件监听
            this.state.mediaQuery.addEventListener('change', handleThemeChange);
            this.state.eventListeners.add({
                target: this.state.mediaQuery,
                type: 'change',
                handler: handleThemeChange
            });

            this.state.enabled = true;

            // 添加原生主题支持的 meta 标签
            this.addColorSchemeMeta();
        }

        /**
         * 设置主题 cookie
         * @param {string} theme - 主题值
         */
        setThemeCookie(theme) {
            try {
                const { NAME, DOMAINS, MAX_AGE } = this.CONFIG.COOKIE;
                const cookieString = `${NAME}=${theme}; path=/; secure; max-age=${MAX_AGE}`;

                DOMAINS.forEach(domain => {
                    document.cookie = `${cookieString}; domain=${domain}`;
                });

                this.log(`Theme cookie set to: ${theme}`);
            } catch (error) {
                this.log(`Failed to set theme cookie: ${error.message}`, 'error');
            }
        }

        /**
         * 添加 color-scheme meta 标签
         */
        addColorSchemeMeta() {
            if (!document.querySelector('meta[name="color-scheme"]')) {
                const meta = document.createElement('meta');
                meta.name = 'color-scheme';
                meta.content = 'light dark';
                document.head.appendChild(meta);
                this.log('Added color-scheme meta tag');
            }
        }

        /**
         * 禁用主题管理器
         */
        disable() {
            if (!this.state.enabled) return;

            // 清理所有事件监听
            this.state.eventListeners.forEach(({ target, type, handler }) => {
                target.removeEventListener(type, handler);
            });
            this.state.eventListeners.clear();

            this.state.enabled = false;
            this.log('Theme manager disabled');
        }

        /**
         * 日志工具方法
         * @param {string} message - 日志消息
         * @param {'log'|'warn'|'error'} level - 日志级别
         */
        log(message, level = 'log') {
            if (!this.CONFIG.DEBUG) return;

            const prefix = '[X Theme Manager]';
            switch (level) {
                case 'warn':
                    console.warn(`${prefix} ${message}`);
                    break;
                case 'error':
                    console.error(`${prefix} ${message}`);
                    break;
                default:
                    console.log(`${prefix} ${message}`);
            }
        }
    }

    // 脚本启动逻辑
    const startScript = () => {
        const themeManager = new XThemeManager();

        // 将实例绑定到 window 对象,方便调试
        if (themeManager.CONFIG.DEBUG) {
            window.__X_THEME_MANAGER__ = themeManager;
        }

        // 初始化主题管理器
        themeManager.initialize().catch(error => {
            console.error('[X Theme Manager] Failed to initialize:', error);
        });
    };

    // 根据文档加载状态决定启动时机
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startScript);
    } else {
        startScript();
    }
})();