通用夜间模式切换器

在任何网页上切换夜间模式,使用filter方案,不破坏原有样式

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         通用夜间模式切换器
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  在任何网页上切换夜间模式,使用filter方案,不破坏原有样式
// @author       Llldmiao
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const VERSION = '1.0.1';

    // 配置项
    const CONFIG = {
        storageKey: 'darkModeEnabled',
        positionKey: 'darkModeButtonPosition',
        attributeName: 'data-dark-mode',
        filterValue: 'brightness(0.5)', // 降低亮度实现夜间模式
        transitionDuration: '0.3s',
        buttonSize: '48px',
        defaultPosition: { bottom: 20, right: 20 },
        buttonZIndex: 99999,
        dragThreshold: 5 // 拖动阈值(像素)
    };

    // 全局状态管理
    let stylesInjected = false;
    let eventListeners = {
        mousemove: null,
        mouseup: null,
        touchmove: null,
        touchend: null
    };
    let mutationObserver = null;

    // 添加样式(防止重复注入)
    function addStyles() {
        if (stylesInjected) return;
        stylesInjected = true;

        const css = `
            /* 基础夜间模式样式 - 直接在html上应用brightness降低亮度 */
            html[${CONFIG.attributeName}="true"] {
                filter: ${CONFIG.filterValue} !important;
                transition: filter ${CONFIG.transitionDuration} ease !important;
            }

            /* 滚动条样式优化 - WebKit内核浏览器 (Chrome, Safari, Edge) */
            html[${CONFIG.attributeName}="true"]::-webkit-scrollbar,
            html[${CONFIG.attributeName}="true"] body::-webkit-scrollbar {
                width: 12px !important;
                height: 12px !important;
            }

            html[${CONFIG.attributeName}="true"]::-webkit-scrollbar-track,
            html[${CONFIG.attributeName}="true"] body::-webkit-scrollbar-track {
                background: #1a1a1a !important;
            }

            html[${CONFIG.attributeName}="true"]::-webkit-scrollbar-thumb,
            html[${CONFIG.attributeName}="true"] body::-webkit-scrollbar-thumb {
                background: #4a4a4a !important;
                border-radius: 6px !important;
                border: 2px solid #1a1a1a !important;
            }

            html[${CONFIG.attributeName}="true"]::-webkit-scrollbar-thumb:hover,
            html[${CONFIG.attributeName}="true"] body::-webkit-scrollbar-thumb:hover {
                background: #5a5a5a !important;
            }

            html[${CONFIG.attributeName}="true"]::-webkit-scrollbar-thumb:active,
            html[${CONFIG.attributeName}="true"] body::-webkit-scrollbar-thumb:active {
                background: #6a6a6a !important;
            }

            html[${CONFIG.attributeName}="true"]::-webkit-scrollbar-corner,
            html[${CONFIG.attributeName}="true"] body::-webkit-scrollbar-corner {
                background: #1a1a1a !important;
            }

            /* 滚动条样式优化 - Firefox */
            html[${CONFIG.attributeName}="true"],
            html[${CONFIG.attributeName}="true"] body {
                scrollbar-width: thin !important;
                scrollbar-color: #4a4a4a #1a1a1a !important;
            }

            /* 切换按钮样式 */
            .dark-mode-toggle-btn {
                position: fixed !important;
                width: ${CONFIG.buttonSize} !important;
                height: ${CONFIG.buttonSize} !important;
                border-radius: 50% !important;
                background: linear-gradient(135deg, rgba(102, 126, 234, 0.95), rgba(118, 75, 162, 0.95)) !important;
                backdrop-filter: blur(10px) !important;
                border: 2px solid rgba(255, 255, 255, 0.3) !important;
                cursor: move !important;
                z-index: ${CONFIG.buttonZIndex} !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
                font-size: 24px !important;
                color: #fff !important;
                box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4), 0 0 0 0 rgba(102, 126, 234, 0.4) !important;
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
                user-select: none !important;
                -webkit-user-select: none !important;
                touch-action: none !important;
            }

            .dark-mode-toggle-btn:hover {
                background: linear-gradient(135deg, rgba(102, 126, 234, 1), rgba(118, 75, 162, 1)) !important;
                transform: scale(1.1) !important;
                box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6), 0 0 0 8px rgba(102, 126, 234, 0.1) !important;
            }

            .dark-mode-toggle-btn:active {
                transform: scale(0.95) !important;
                cursor: grabbing !important;
            }

            .dark-mode-toggle-btn.dragging {
                transition: none !important;
                cursor: grabbing !important;
                opacity: 0.9 !important;
            }

            /* 夜间模式拖动时保持更高亮度 */
            html[${CONFIG.attributeName}="true"] .dark-mode-toggle-btn.dragging {
                opacity: 1 !important;
            }

            /* 夜间模式下的按钮样式 - 按钮图标需要保持正常亮度,背景色和日间模式一样 */
            html[${CONFIG.attributeName}="true"] .dark-mode-toggle-btn {
                background: linear-gradient(135deg, rgba(102, 126, 234, 1), rgba(118, 75, 162, 1)) !important;
                border-color: rgba(255, 255, 255, 0.6) !important;
                box-shadow: 0 6px 30px rgba(102, 126, 234, 0.8), 0 0 0 0 rgba(102, 126, 234, 0.5) !important;
            }

            html[${CONFIG.attributeName}="true"] .dark-mode-toggle-btn:hover {
                background: linear-gradient(135deg, rgba(102, 126, 234, 1), rgba(118, 75, 162, 1)) !important;
                box-shadow: 0 10px 40px rgba(102, 126, 234, 1), 0 0 0 12px rgba(102, 126, 234, 0.2) !important;
                transform: scale(1.15) !important;
            }
        `;

        if (typeof GM_addStyle !== 'undefined') {
            GM_addStyle(css);
        } else {
            const style = document.createElement('style');
            style.textContent = css;
            (document.head || document.documentElement).appendChild(style);
        }
    }

    // 获取存储的状态
    function getStoredState() {
        if (typeof GM_getValue !== 'undefined') {
            return GM_getValue(CONFIG.storageKey, false);
        }
        try {
            const stored = localStorage.getItem(CONFIG.storageKey);
            return stored === 'true';
        } catch (e) {
            return false;
        }
    }

    // 保存状态
    function saveState(enabled) {
        if (typeof GM_setValue !== 'undefined') {
            GM_setValue(CONFIG.storageKey, enabled);
        } else {
            try {
                localStorage.setItem(CONFIG.storageKey, enabled.toString());
            } catch (e) {
                console.warn('无法保存夜间模式状态:', e);
            }
        }
    }

    // 获取按钮位置
    function getButtonPosition() {
        if (typeof GM_getValue !== 'undefined') {
            return GM_getValue(CONFIG.positionKey, CONFIG.defaultPosition);
        }
        try {
            const stored = localStorage.getItem(CONFIG.positionKey);
            return stored ? JSON.parse(stored) : CONFIG.defaultPosition;
        } catch (e) {
            return CONFIG.defaultPosition;
        }
    }

    // 保存按钮位置
    function saveButtonPosition(position) {
        if (typeof GM_setValue !== 'undefined') {
            GM_setValue(CONFIG.positionKey, position);
        } else {
            try {
                localStorage.setItem(CONFIG.positionKey, JSON.stringify(position));
            } catch (e) {
                console.warn('无法保存按钮位置:', e);
            }
        }
    }

    // 切换夜间模式
    function toggleDarkMode() {
        const html = document.documentElement;
        const currentState = html.getAttribute(CONFIG.attributeName) === 'true';
        const newState = !currentState;

        html.setAttribute(CONFIG.attributeName, newState.toString());
        saveState(newState);

        // 更新按钮图标
        updateButtonIcon(newState);

        return newState;
    }

    // 应用夜间模式状态
    function applyDarkModeState(enabled) {
        const html = document.documentElement;
        html.setAttribute(CONFIG.attributeName, enabled.toString());
        updateButtonIcon(enabled);
    }

    // 移除事件监听器
    function removeEventListeners() {
        if (eventListeners.mousemove) {
            document.removeEventListener('mousemove', eventListeners.mousemove);
            eventListeners.mousemove = null;
        }
        if (eventListeners.mouseup) {
            document.removeEventListener('mouseup', eventListeners.mouseup);
            eventListeners.mouseup = null;
        }
        if (eventListeners.touchmove) {
            document.removeEventListener('touchmove', eventListeners.touchmove);
            eventListeners.touchmove = null;
        }
        if (eventListeners.touchend) {
            document.removeEventListener('touchend', eventListeners.touchend);
            eventListeners.touchend = null;
        }
    }

    // 创建切换按钮
    function createToggleButton() {
        // 先移除旧的事件监听器,防止泄漏
        removeEventListeners();

        const button = document.createElement('div');
        button.className = 'dark-mode-toggle-btn';
        button.setAttribute('role', 'button');
        button.setAttribute('aria-label', '切换夜间模式');
        button.setAttribute('title', '点击切换,拖动调整位置');
        
        // 初始图标
        const isDarkMode = getStoredState();
        button.textContent = isDarkMode ? '☀️' : '🌙';
        
        // 设置初始位置
        const position = getButtonPosition();
        applyButtonPosition(button, position);
        
        // 拖动相关变量
        let isDragging = false;
        let startX, startY;
        let startLeft, startTop;
        let hasMoved = false;

        // 通用拖动处理函数
        function handleDragMove(clientX, clientY) {
            if (!isDragging) return;
            
            const deltaX = clientX - startX;
            const deltaY = clientY - startY;
            
            // 判断是否真的在拖动(移动超过阈值)
            if (Math.abs(deltaX) > CONFIG.dragThreshold || Math.abs(deltaY) > CONFIG.dragThreshold) {
                hasMoved = true;
            }
            
            // 计算新位置
            let newLeft = startLeft + deltaX;
            let newTop = startTop + deltaY;
            
            // 边界检测
            const buttonSize = parseInt(CONFIG.buttonSize);
            newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - buttonSize));
            newTop = Math.max(0, Math.min(newTop, window.innerHeight - buttonSize));
            
            button.style.left = newLeft + 'px';
            button.style.top = newTop + 'px';
            button.style.right = 'auto';
            button.style.bottom = 'auto';
        }

        // 通用拖动结束处理函数
        function handleDragEnd() {
            if (!isDragging) return;
            
            isDragging = false;
            button.classList.remove('dragging');
            
            // 如果移动了,保存位置;否则触发点击
            if (hasMoved) {
                const rect = button.getBoundingClientRect();
                const newPosition = {
                    left: rect.left,
                    top: rect.top,
                    right: window.innerWidth - rect.right,
                    bottom: window.innerHeight - rect.bottom
                };
                saveButtonPosition(newPosition);
            } else {
                // 没有拖动,触发点击切换
                toggleDarkMode();
            }
        }
        
        // 鼠标按下
        button.addEventListener('mousedown', (e) => {
            e.preventDefault();
            isDragging = true;
            hasMoved = false;
            
            startX = e.clientX;
            startY = e.clientY;
            
            const rect = button.getBoundingClientRect();
            startLeft = rect.left;
            startTop = rect.top;
            
            button.classList.add('dragging');
        });
        
        // 鼠标移动(保存引用)
        eventListeners.mousemove = (e) => handleDragMove(e.clientX, e.clientY);
        document.addEventListener('mousemove', eventListeners.mousemove);
        
        // 鼠标释放(保存引用)
        eventListeners.mouseup = handleDragEnd;
        document.addEventListener('mouseup', eventListeners.mouseup);
        
        // 触摸开始
        button.addEventListener('touchstart', (e) => {
            e.preventDefault();
            isDragging = true;
            hasMoved = false;
            
            const touch = e.touches[0];
            startX = touch.clientX;
            startY = touch.clientY;
            
            const rect = button.getBoundingClientRect();
            startLeft = rect.left;
            startTop = rect.top;
            
            button.classList.add('dragging');
        });
        
        // 触摸移动(保存引用)
        eventListeners.touchmove = (e) => {
            const touch = e.touches[0];
            handleDragMove(touch.clientX, touch.clientY);
        };
        document.addEventListener('touchmove', eventListeners.touchmove, { passive: false });
        
        // 触摸结束(保存引用)
        eventListeners.touchend = handleDragEnd;
        document.addEventListener('touchend', eventListeners.touchend);

        // 添加到页面(使用 fallback)
        const container = document.body || document.documentElement;
        container.appendChild(button);
        return button;
    }
    
    // 应用按钮位置
    function applyButtonPosition(button, position) {
        // 优先使用 bottom/right,如果没有则使用 top/left
        if (position.bottom !== undefined && position.right !== undefined) {
            button.style.bottom = position.bottom + 'px';
            button.style.right = position.right + 'px';
            button.style.top = 'auto';
            button.style.left = 'auto';
        } else if (position.top !== undefined && position.left !== undefined) {
            button.style.top = position.top + 'px';
            button.style.left = position.left + 'px';
            button.style.bottom = 'auto';
            button.style.right = 'auto';
        } else {
            button.style.bottom = CONFIG.defaultPosition.bottom + 'px';
            button.style.right = CONFIG.defaultPosition.right + 'px';
        }
    }

    // 更新按钮图标
    function updateButtonIcon(isDarkMode) {
        const button = document.querySelector('.dark-mode-toggle-btn');
        if (button) {
            button.textContent = isDarkMode ? '☀️' : '🌙';
        }
    }

    // 快捷键支持
    function setupKeyboardShortcut() {
        document.addEventListener('keydown', (e) => {
            // Ctrl+Shift+D 或 Cmd+Shift+D (Mac)
            if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') {
                e.preventDefault();
                e.stopPropagation();
                toggleDarkMode();
            }
        }, true);
    }

    // 节流函数
    function throttle(func, delay) {
        let timer = null;
        return function(...args) {
            if (timer) return;
            timer = setTimeout(() => {
                func.apply(this, args);
                timer = null;
            }, delay);
        };
    }

    // 清理资源
    function destroy() {
        try {
            // 移除按钮
            const button = document.querySelector('.dark-mode-toggle-btn');
            if (button) {
                button.remove();
            }
            
            // 移除事件监听器
            removeEventListeners();
            
            // 停止 MutationObserver
            if (mutationObserver) {
                mutationObserver.disconnect();
                mutationObserver = null;
            }
            
            console.log(`[夜间模式] v${VERSION} 已卸载`);
        } catch (error) {
            console.error('[夜间模式] 卸载失败:', error);
        }
    }

    // 立即添加样式(在页面渲染前)
    addStyles();

    // 立即应用夜间模式状态(在页面渲染前)
    // 这样可以避免从亮色闪到暗色的问题
    (function applyDarkModeImmediately() {
        const isDarkMode = getStoredState();
        if (isDarkMode) {
            // 立即在 documentElement 上设置属性
            if (document.documentElement) {
                document.documentElement.setAttribute(CONFIG.attributeName, 'true');
            }
        }
    })();

    // 初始化
    function init() {
        try {
            console.log(`[夜间模式] v${VERSION} 初始化中...`);

            // 等待DOM加载完成
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => {
                    // 确保状态正确应用
                    applyDarkModeState(getStoredState());
                    createToggleButton();
                    setupKeyboardShortcut();
                    setupSPAListener();
                    console.log(`[夜间模式] v${VERSION} 已加载`);
                });
            } else {
                // 确保状态正确应用
                applyDarkModeState(getStoredState());
                createToggleButton();
                setupKeyboardShortcut();
                setupSPAListener();
                console.log(`[夜间模式] v${VERSION} 已加载`);
            }
        } catch (error) {
            console.error('[夜间模式] 初始化失败:', error);
        }
    }

    // 设置 SPA 路由监听(优化性能)
    function setupSPAListener() {
        let lastUrl = location.href;
        
        // 使用节流的 URL 检查函数
        const checkUrlChange = throttle(() => {
            const url = location.href;
            if (url !== lastUrl) {
                lastUrl = url;
                // 确保按钮存在
                if (!document.querySelector('.dark-mode-toggle-btn')) {
                    createToggleButton();
                }
                // 重新应用状态
                applyDarkModeState(getStoredState());
            }
        }, 1000);

        // 监听 popstate 和 pushstate 事件(更高效的 SPA 检测)
        window.addEventListener('popstate', checkUrlChange);
        
        // 拦截 pushState 和 replaceState
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;
        
        history.pushState = function(...args) {
            originalPushState.apply(this, args);
            checkUrlChange();
        };
        
        history.replaceState = function(...args) {
            originalReplaceState.apply(this, args);
            checkUrlChange();
        };
        
        // 作为后备方案,使用节流的 MutationObserver
        mutationObserver = new MutationObserver(checkUrlChange);
        mutationObserver.observe(document.body || document.documentElement, {
            childList: true,
            subtree: false // 只监听直接子元素,减少性能开销
        });
    }

    // 立即执行初始化
    init();

    // 暴露清理方法到全局(可选,便于调试)
    if (typeof window !== 'undefined') {
        window.darkModeDestroy = destroy;
    }

})();