Greasy Fork

Greasy Fork is available in English.

网站暗黑模式切换器

一键切换网站暗黑/日间模式,支持拖拽吸附与边缘图标化

// ==UserScript==
// @name         网站暗黑模式切换器
// @version      1.6
// @description  一键切换网站暗黑/日间模式,支持拖拽吸附与边缘图标化
// @author       Your Name
// @match        *://*/*
// @grant        none
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function() {
    'use strict';

    // 排除iframe场景
    if (window !== window.top) return;

    // 常量定义
    const STORAGE_KEY = 'darkMode';
    const POSITION_KEY = 'darkModeButtonPosition';
    const SNAP_DISTANCE = 10;    // 吸附触发距离
    const ICON_ONLY_DISTANCE = 15; // 图标化触发距离
    const DRAG_THRESHOLD = 3;    // 拖拽阈值
    const FULL_PADDING = '10px 15px'; // 完整显示时的内边距
    const ICON_PADDING = '10px';   // 图标显示时的内边距

    // 检查本地存储的模式偏好
    const isDarkMode = localStorage.getItem(STORAGE_KEY) === 'true';

    // 创建切换按钮
    const toggleBtn = document.createElement('button');
    toggleBtn.id = 'dark-mode-toggle';
    toggleBtn.style.position = 'fixed';
    toggleBtn.style.zIndex = '9999';
    toggleBtn.style.padding = FULL_PADDING;
    toggleBtn.style.border = 'none';
    toggleBtn.style.borderRadius = '20px';
    toggleBtn.style.cursor = 'move';
    toggleBtn.style.fontWeight = 'bold';
    toggleBtn.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
    toggleBtn.style.transition = 'all 0.3s ease-out';
    toggleBtn.style.userSelect = 'none';

    // 应用初始模式
    function applyMode(dark) {
        const html = document.documentElement;
        if (dark) {
            html.style.filter = 'invert(1) hue-rotate(180deg)';
            toggleBtn.dataset.mode = 'light';
            toggleBtn.innerHTML = '<span class="full-text">🌞 日间模式</span><span class="icon-text">🌞</span>';
            toggleBtn.style.backgroundColor = '#fff';
            toggleBtn.style.color = '#000';
        } else {
            html.style.filter = 'none';
            toggleBtn.dataset.mode = 'dark';
            toggleBtn.innerHTML = '<span class="full-text">🌙 暗黑模式</span><span class="icon-text">🌙</span>';
            toggleBtn.style.backgroundColor = '#333';
            toggleBtn.style.color = '#fff';
        }
        // 处理媒体元素复原
        document.querySelectorAll('img, video, iframe, svg, canvas, .avatar, .logo, .icon').forEach(el => {
            el.style.filter = dark ? 'invert(1) hue-rotate(180deg)' : 'none';
        });
        localStorage.setItem(STORAGE_KEY, dark);
    }

    // 恢复保存的位置
    function restorePosition() {
        const position = localStorage.getItem(POSITION_KEY);
        if (position) {
            const { left, top } = JSON.parse(position);
            toggleBtn.style.left = `${left}px`;
            toggleBtn.style.top = `${top}px`;
        } else {
            // 默认位置
            toggleBtn.style.right = '20px';
            toggleBtn.style.bottom = '20px';
        }
    }

    // 保存当前位置
    function savePosition() {
        const rect = toggleBtn.getBoundingClientRect();
        localStorage.setItem(POSITION_KEY, JSON.stringify({
            left: rect.left,
            top: rect.top
        }));
    }

    // 边缘检测与图标化
    function checkEdgePosition() {
        const rect = toggleBtn.getBoundingClientRect();
        const windowWidth = window.innerWidth;
        const windowHeight = window.innerHeight;

        // 计算与各边的距离
        const distances = {
            left: rect.left,
            right: windowWidth - rect.right,
            top: rect.top,
            bottom: windowHeight - rect.bottom
        };

        // 找到最近的边
        let closestSide = 'left';
        let minDistance = distances.left;

        for (const side in distances) {
            if (distances[side] < minDistance) {
                minDistance = distances[side];
                closestSide = side;
            }
        }

        // 应用图标化效果
        const isIconMode = minDistance < ICON_ONLY_DISTANCE;

        if (isIconMode) {
            toggleBtn.style.padding = ICON_PADDING;
            toggleBtn.querySelector('.full-text').style.display = 'none';
            toggleBtn.querySelector('.icon-text').style.display = 'inline';

            // 根据边调整样式并紧贴边缘
            switch (closestSide) {
                case 'left':
                    toggleBtn.style.left = '0px';
                    toggleBtn.style.borderTopLeftRadius = '0';
                    toggleBtn.style.borderBottomLeftRadius = '0';
                    break;
                case 'right':
                    toggleBtn.style.right = '0px';
                    toggleBtn.style.left = 'auto';
                    toggleBtn.style.borderTopRightRadius = '0';
                    toggleBtn.style.borderBottomRightRadius = '0';
                    break;
                case 'top':
                    toggleBtn.style.top = '0px';
                    toggleBtn.style.borderTopLeftRadius = '0';
                    toggleBtn.style.borderTopRightRadius = '0';
                    break;
                case 'bottom':
                    toggleBtn.style.bottom = '0px';
                    toggleBtn.style.borderBottomLeftRadius = '0';
                    toggleBtn.style.borderBottomRightRadius = '0';
                    break;
            }
        } else {
            toggleBtn.style.padding = FULL_PADDING;
            toggleBtn.querySelector('.full-text').style.display = 'inline';
            toggleBtn.querySelector('.icon-text').style.display = 'none';
            toggleBtn.style.borderRadius = '20px';
        }

        return isIconMode;
    }

    // 拖拽状态变量
    let isDragging = false;
    let startX, startY, offsetX, offsetY;
    let btnWidth, btnHeight;
    let windowWidth, windowHeight;

    // 吸附到最近的边缘
    function snapToEdge() {
        const rect = toggleBtn.getBoundingClientRect();
        const distances = {
            left: rect.left,
            right: windowWidth - rect.right,
            top: rect.top,
            bottom: windowHeight - rect.bottom
        };

        // 找到最近的边
        let closestSide = 'left';
        let minDistance = distances.left;

        for (const side in distances) {
            if (distances[side] < minDistance) {
                minDistance = distances[side];
                closestSide = side;
            }
        }

        // 如果足够近则吸附
        if (minDistance < SNAP_DISTANCE) {
            // 如果是图标模式则完全贴边,否则保留小间距
            const offset = checkEdgePosition() ? 0 : 5;

            switch (closestSide) {
                case 'left':
                    toggleBtn.style.left = `${offset}px`;
                    break;
                case 'right':
                    // 修复右边吸附逻辑
                    toggleBtn.style.right = `${offset}px`;
                    toggleBtn.style.left = 'auto';
                    break;
                case 'top':
                    toggleBtn.style.top = `${offset}px`;
                    break;
                case 'bottom':
                    toggleBtn.style.top = 'auto';
                    toggleBtn.style.bottom = `${offset}px`;
                    break;
            }
            return true; // 已吸附
        }
        return false; // 未吸附
    }

    // 实现拖拽功能
    toggleBtn.addEventListener('mousedown', (e) => {
        if (e.button !== 0) return;

        isDragging = true;
        startX = e.clientX;
        startY = e.clientY;

        const rect = toggleBtn.getBoundingClientRect();
        offsetX = e.clientX - rect.left;
        offsetY = e.clientY - rect.top;
        btnWidth = rect.width;
        btnHeight = rect.height;
        windowWidth = window.innerWidth;
        windowHeight = window.innerHeight;

        // 提升拖拽时的视觉反馈
        toggleBtn.style.transform = 'scale(1.1)';
        toggleBtn.style.transition = 'transform 0.1s, padding 0.3s, borderRadius 0.3s';

        // 防止拖拽时选中文本
        e.preventDefault();
    });

    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;

        // 计算新位置
        let newX = e.clientX - offsetX;
        let newY = e.clientY - offsetY;

        // 边界限制
        newX = Math.max(0, Math.min(newX, windowWidth - btnWidth));
        newY = Math.max(0, Math.min(newY, windowHeight - btnHeight));

        // 应用新位置
        toggleBtn.style.right = 'auto';
        toggleBtn.style.bottom = 'auto';
        toggleBtn.style.left = `${newX}px`;
        toggleBtn.style.top = `${newY}px`;

        // 实时边缘检测与吸附
        if (!snapToEdge()) {
            checkEdgePosition();
        }
    });

    document.addEventListener('mouseup', (e) => {
        if (!isDragging) return;

        isDragging = false;
        toggleBtn.style.transform = 'scale(1)';
        toggleBtn.style.transition = 'all 0.3s ease-out';

        // 执行最终吸附
        snapToEdge();

        // 保存新位置
        savePosition();

        // 只有微小移动时视为点击
        const moveX = Math.abs(e.clientX - startX);
        const moveY = Math.abs(e.clientY - startY);

        if (moveX < DRAG_THRESHOLD && moveY < DRAG_THRESHOLD) {
            const current = localStorage.getItem(STORAGE_KEY) === 'true';
            applyMode(!current);
        }
    });

    // 窗口大小改变时重新计算位置
    window.addEventListener('resize', () => {
        const rect = toggleBtn.getBoundingClientRect();
        const newWindowWidth = window.innerWidth;
        const newWindowHeight = window.innerHeight;

        // 检查按钮是否超出边界,如有则调整位置
        if (rect.right > newWindowWidth) {
            toggleBtn.style.left = 'auto';
            toggleBtn.style.right = '0px';
        }

        if (rect.bottom > newWindowHeight) {
            toggleBtn.style.top = 'auto';
            toggleBtn.style.bottom = '0px';
        }

        // 保存调整后的位置
        savePosition();
        // 边缘检测
        checkEdgePosition();
    });

    // 初始化
    toggleBtn.innerHTML = '<span class="full-text">🌙 暗黑模式</span><span class="icon-text">🌙</span>';
    toggleBtn.querySelector('.icon-text').style.display = 'none';
    document.body.appendChild(toggleBtn);
    restorePosition();
    applyMode(isDarkMode);
    checkEdgePosition();
})();