Greasy Fork

Via Css隐藏规则日志(高斯模糊)

检测哪些Css规则在Via上生效,并输出匹配日志。支持动态检测开关。显示结果使用 iOS 风格高斯模糊提示框,支持自适应宽度和滚动,匹配结果双击消失。

目前为 2025-04-17 提交的版本。查看 最新版本

// ==UserScript==
// @name         Via Css隐藏规则日志(高斯模糊)
// @namespace    https://viayoo.com/
// @version      0.1
// @license      MIT
// @description  检测哪些Css规则在Via上生效,并输出匹配日志。支持动态检测开关。显示结果使用 iOS 风格高斯模糊提示框,支持自适应宽度和滚动,匹配结果双击消失。
// @author       Copilot & Grok
// @run-at       document-end
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const BUTTON_STORAGE = {
        ENABLED: 'floatingButtonEnabled',
        LEFT: 'floatingButtonLeft',
        TOP: 'floatingButtonTop',
        DYNAMIC_OBSERVER_ENABLED: 'dynamicObserverEnabled'
    };
    const DEFAULT_CSS_FILE_PATH = '/via_inject_blocker.css';
    const LONG_PRESS_THRESHOLD = 500;
    const OBSERVER_INTERVAL = 2000;
    const BATCH_SIZE = 100;

    const createStyledElement = (tag, styles, text) => {
        const el = document.createElement(tag);
        Object.assign(el.style, styles);
        if (text) el.textContent = text;
        return el;
    };

    const splitSelectors = cssText => {
        const selectors = [];
        let current = '';
        let inBlock = false;
        let bracketDepth = 0;
        let parenDepth = 0;
        let inQuote = false;
        let quoteChar = null;

        for (let i = 0; i < cssText.length; i++) {
            const char = cssText[i];

            if (inQuote) {
                current += char;
                if (char === quoteChar) inQuote = false;
                continue;
            }

            if (char === '"' || char === "'") {
                inQuote = true;
                quoteChar = char;
                current += char;
                continue;
            }

            if (inBlock) {
                if (char === '}') inBlock = false;
                continue;
            }

            if (char === '[') bracketDepth++;
            if (char === ']') bracketDepth--;
            if (char === '(') parenDepth++;
            if (char === ')') parenDepth--;
            if (char === '{' && bracketDepth === 0 && parenDepth === 0 && !inQuote) {
                if (current.trim()) selectors.push(current.trim());
                current = '';
                inBlock = true;
                continue;
            }
            if (char === ',' && bracketDepth === 0 && parenDepth === 0 && !inQuote && !inBlock) {
                if (current.trim()) selectors.push(current.trim());
                current = '';
            } else {
                current += char;
            }
        }

        if (current.trim() && !inBlock) selectors.push(current.trim());
        return selectors.filter(s => s && !s.includes('!important') && !s.startsWith('@'));
    };

    const checkActiveSelectors = async (cssText) => {
        try {
            const selectors = splitSelectors(cssText);
            const activeRules = [];
            const debugInfo = [];

            for (let i = 0; i < selectors.length; i += BATCH_SIZE) {
                const batch = selectors.slice(i, i + BATCH_SIZE);
                for (const selector of batch) {
                    try {
                        const elements = document.querySelectorAll(selector);
                        if (elements.length) {
                            activeRules.push({
                                selector: selector.trim(),
                                count: elements.length
                            });
                        }
                        debugInfo.push({
                            selector: selector.trim(),
                            count: elements.length,
                            exists: elements.length > 0 ? '匹配成功' : '无匹配元素'
                        });
                    } catch (e) {
                        debugInfo.push({
                            selector: selector.trim(),
                            count: 0,
                            exists: `选择器无效: ${e.message}`
                        });
                    }
                }
                await new Promise(resolve => setTimeout(resolve, 0));
            }

            return {
                activeRules,
                debugInfo
            };
        } catch (e) {
            console.error(`[Via CSS Logger] 检查选择器失败: ${e.message}`);
            return {
                activeRules: [],
                debugInfo: []
            };
        }
    };

    const showBlurToast = (message, duration = 10000, isPrimary = false) => {
        const existingToast = document.querySelector('.via-blur-toast');
        if (existingToast) existingToast.remove();

        const toast = createStyledElement('div', {
            position: 'fixed',
            left: '50%',
            transform: 'translateX(-50%) scale(0.8)', // 初始缩小
            maxWidth: 'min(100vw, 600px)',
            maxHeight: isPrimary ? '80vh' : '50vh',
            padding: '15px 20px',
            backgroundColor: 'rgba(255, 255, 255, 0.7)',
            backdropFilter: 'blur(12px)',
            WebkitBackdropFilter: 'blur(12px)',
            borderRadius: '12px',
            border: '1px solid rgba(255, 255, 255, 0.3)',
            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
            color: '#333',
            fontSize: '16px',
            fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
            zIndex: '10001',
            opacity: '0',
            transition: 'all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55)', // 回弹效果曲线
            whiteSpace: 'pre-wrap',
            textAlign: isPrimary ? 'left' : 'center',
            lineHeight: '1.5',
            cursor: 'pointer',
            boxSizing: 'border-box',
            overflowY: isPrimary ? 'auto' : 'hidden',
            scrollbarWidth: 'thin',
            scrollbarColor: 'rgba(0, 0, 0, 0.3) transparent'
        }, message);

        // 自定义滚动条样式
        const styleSheet = document.createElement('style');
        styleSheet.textContent = `
        .via-blur-toast::-webkit-scrollbar {
            width: 8px;
        }
        .via-blur-toast::-webkit-scrollbar-track {
            background: transparent;
        }
        .via-blur-toast::-webkit-scrollbar-thumb {
            background: rgba(0, 0, 0, 0.3);
            border-radius: 9px;
        }
        .via-blur-toast::-webkit-scrollbar-thumb:hover {
            background: rgba(0, 0, 0, 0.1);
        }
    `;
        document.head.appendChild(styleSheet);

        if (isPrimary) {
            toast.style.top = '50%';
            toast.style.transform = 'translate(-50%, -50%) scale(0.8)';
        } else {
            toast.style.top = '85%';
        }

        toast.className = 'via-blur-toast';
        document.body.appendChild(toast);

        // 入场动画
        setTimeout(() => {
            toast.style.opacity = '1';
            toast.style.transform = isPrimary ? 'translate(-50%, -50%) scale(1)' : 'translateX(-50%) scale(1)';
        }, 10);

        // 离场动画
        const timeout = setTimeout(() => {
            toast.style.opacity = '0';
            toast.style.transform = isPrimary ? 'translate(-50%, -50%) scale(0.8)' : 'translateX(-50%) scale(0.8)';
            setTimeout(() => {
                toast.remove();
                styleSheet.remove();
            }, 500); // 与 transition 持续时间一致
        }, duration);

        // 双击/单击移除
        const removeToast = () => {
            clearTimeout(timeout);
            toast.style.opacity = '0';
            toast.style.transform = isPrimary ? 'translate(-50%, -50%) scale(0.8)' : 'translateX(-50%) scale(0.8)';
            setTimeout(() => {
                toast.remove();
                styleSheet.remove();
            }, 500);
        };

        if (isPrimary) {
            toast.addEventListener('dblclick', removeToast);
        } else {
            toast.addEventListener('click', removeToast);
        }
    };


    const checkCssFile = async (enableDynamic = false) => {
        const cssUrl = `http://${window.location.hostname}${DEFAULT_CSS_FILE_PATH}`;
        console.log(`[Via CSS Logger] 尝试获取 CSS 文件: ${cssUrl}`);
        try {
            const response = await fetch(cssUrl, {
                cache: 'no-cache'
            });
            console.log(`[Via CSS Logger] 获取 CSS 文件,状态码: ${response.status}`);
            if (!response.ok) throw new Error(`状态码: ${response.status}`);

            const rawCss = await response.text();
            if (!rawCss.trim()) throw new Error('CSS文件为空');

            const checkRules = async () => {
                try {
                    const {
                        activeRules
                    } = await checkActiveSelectors(rawCss);

                    if (activeRules.length) {
                        const messageLines = [
                            `🎉 检测完成!共 ${activeRules.length} 条规则生效:`,
                            '--------------------------------',
                            ...activeRules.map((r, i) =>
                                `${i + 1}. 规则: ${window.location.hostname}##${r.selector}\n   匹配数: ${r.count}\n`
                            ),
                            '--------------------------------'
                        ];
                        const fullMessage = messageLines.join('\n');
                        console.log(fullMessage);
                        showBlurToast(fullMessage.slice(0, 2500) + (fullMessage.length > 2500 ? '\n\nℹ️ 日志过长,请查看控制台以获取完整信息' : ''), 10000, true);
                    } else {
                        showBlurToast('⚠️ 没有发现生效的CSS规则!', 3000, false);
                    }

                    if (enableDynamic && GM_getValue(BUTTON_STORAGE.DYNAMIC_OBSERVER_ENABLED, false)) {
                        startDynamicObserver(rawCss);
                    }
                } catch (e) {
                    console.error(`[Via CSS Logger] 规则检查失败: ${e.message}`);
                    showBlurToast(`❌ 规则检查失败: ${e.message}`, 5000, false);
                }
            };

            if (document.readyState === 'complete') {
                await checkRules();
            } else {
                window.addEventListener('load', async () => {
                    await checkRules();
                }, {
                    once: true
                });
            }
        } catch (e) {
            console.error(`[Via CSS Logger] CSS检查失败: ${e.message}`);
            showBlurToast(`❌ 检查CSS文件失败: ${e.message}\nURL: ${cssUrl}`, 5000, false);
        }
    };


    const startDynamicObserver = (cssText) => {
        try {
            let lastCheck = Date.now();
            let hasTriggered = false;
            let isChecking = false;

            const observer = new MutationObserver(async (mutations) => {
                if (hasTriggered || isChecking) return;

                const now = Date.now();
                if (now - lastCheck >= OBSERVER_INTERVAL) {
                    isChecking = true;
                    try {
                        console.log(`[Via CSS Logger] 动态检查触发: ${new Date().toISOString()}, 变化数: ${mutations.length}`);
                        const {
                            activeRules
                        } = await checkActiveSelectors(cssText);
                        if (activeRules.length) {
                            hasTriggered = true;
                            observer.disconnect();
                            const messageLines = [
                                `🎉 动态检测完成!共 ${activeRules.length} 条规则生效:`,
                                '--------------------------------',
                                ...activeRules.map((r, i) =>
                                    `${i + 1}. 规则: ${window.location.hostname}##${r.selector}\n   匹配数: ${r.count}\n`
                                ),
                                '--------------------------------'
                            ];
                            const fullMessage = messageLines.join('\n');
                            console.log(`[Via CSS Logger] 弹窗输出:`, fullMessage);
                            showBlurToast(fullMessage.slice(0, 2500), 10000, true); // 主要消息
                        }
                    } catch (e) {
                        console.error(`[Via CSS Logger] 动态检查失败: ${e.message}`);
                        showBlurToast(`❌ 动态检查失败: ${e.message}`, 5000, false);
                    } finally {
                        isChecking = false;
                        lastCheck = now;
                    }
                }
            });
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        } catch (e) {
            console.error(`[Via CSS Logger] 动态监控启动失败: ${e.message}`);
            showBlurToast(`❌ 动态监控启动失败: ${e.message}`, 5000, false);
        }
    };

    const createFloatingButton = () => {
        if (window.self !== window.top) return;

        const button = createStyledElement('div', {
            position: 'fixed',
            zIndex: '10000',
            width: '80px',
            height: '40px',
            backgroundColor: 'rgba(255, 255, 255, 0.2)',
            backdropFilter: 'blur(16px)',
            WebkitBackdropFilter: 'blur(16px)',
            border: '0.5px solid rgba(255, 255, 255, 0.3)',
            borderRadius: '12px',
            color: '#1C2526',
            textAlign: 'center',
            lineHeight: '40px',
            fontSize: '15px',
            fontFamily: 'SF Pro Text, -apple-system, BlinkMacSystemFont, sans-serif',
            fontWeight: '500',
            boxShadow: '0 4px 16px rgba(0, 0, 0, 0.08), inset 0 1px 1px rgba(255, 255, 255, 0.3)',
            cursor: 'pointer',
            opacity: '0.95',
            transition: 'transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1.5), opacity 0.2s, box-shadow 0.2s',
            touchAction: 'none',
            userSelect: 'none',
            WebkitUserSelect: 'none'
        }, 'CSS日志');

        const defaultLeft = window.innerWidth - 100;
        const defaultTop = window.innerHeight - 100;
        button.style.left = `${GM_getValue(BUTTON_STORAGE.LEFT, defaultLeft)}px`;
        button.style.top = `${GM_getValue(BUTTON_STORAGE.TOP, defaultTop)}px`;

        document.body.appendChild(button);

        let isDragging = false,
            startX,
            startY,
            startLeft,
            startTop,
            touchStartTime;

        button.addEventListener('touchstart', e => {
            e.preventDefault();
            touchStartTime = Date.now();
            isDragging = false;
            const touch = e.touches[0];
            startX = touch.clientX;
            startY = touch.clientY;
            startLeft = parseInt(button.style.left) || 0;
            startTop = parseInt(button.style.top) || 0;

            button.style.transform = 'scale(0.95)';
            button.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1), inset 0 1px 1px rgba(255, 255, 255, 0.3)';
        });

        button.addEventListener('touchmove', e => {
            e.preventDefault();
            const touch = e.touches[0];
            const deltaX = touch.clientX - startX;
            const deltaY = touch.clientY - startY;

            if (Date.now() - touchStartTime >= LONG_PRESS_THRESHOLD) {
                isDragging = true;
                const newLeft = startLeft + deltaX;
                const newTop = startTop + deltaY;
                const rect = button.getBoundingClientRect();
                button.style.left = `${Math.max(0, Math.min(newLeft, window.innerWidth - rect.width))}px`;
                button.style.top = `${Math.max(0, Math.min(newTop, window.innerHeight - rect.height))}px`;
            }
        });

        button.addEventListener('touchend', e => {
            e.preventDefault();
            const touchDuration = Date.now() - touchStartTime;
            button.style.transform = 'scale(1)';
            button.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.08), inset 0 1px 1px rgba(255, 255, 255, 0.3)';

            if (isDragging && touchDuration >= LONG_PRESS_THRESHOLD) {
                const rect = button.getBoundingClientRect();
                const newLeft = rect.left + rect.width / 2 < window.innerWidth / 2 ? 0 : window.innerWidth - rect.width;
                button.style.left = `${newLeft}px`;
                GM_setValue(BUTTON_STORAGE.LEFT, newLeft);
                GM_setValue(BUTTON_STORAGE.TOP, parseInt(button.style.top));
            } else if (touchDuration < LONG_PRESS_THRESHOLD) {
                showBlurToast('正在匹配对应规则……', 2000, false);
                checkCssFile(GM_getValue(BUTTON_STORAGE.DYNAMIC_OBSERVER_ENABLED, false));
            }
        });

        return button;
    };

    const ensureButtonExists = () => {
        if (!document.querySelector("div[style*='CSS日志']")) {
            createFloatingButton();
        }
    };

    const resetButtonPosition = () => {
        const defaultLeft = window.innerWidth - 100;
        const defaultTop = window.innerHeight - 100;
        GM_setValue(BUTTON_STORAGE.LEFT, defaultLeft);
        GM_setValue(BUTTON_STORAGE.TOP, defaultTop);

        const button = document.querySelector("div[style*='CSS日志']");
        if (button) {
            button.style.left = `${defaultLeft}px`;
            button.style.top = `${defaultTop}px`;
        }
        showBlurToast('✅ 悬浮按钮位置已重置至默认位置!', 3000, false);
    };

    const init = () => {
        try {
            const isButtonEnabled = GM_getValue(BUTTON_STORAGE.ENABLED, false);
            const isDynamicObserverEnabled = GM_getValue(BUTTON_STORAGE.DYNAMIC_OBSERVER_ENABLED, false);

            GM_registerMenuCommand(
                isButtonEnabled ? '关闭悬浮按钮' : '开启悬浮按钮',
                () => {
                    GM_setValue(BUTTON_STORAGE.ENABLED, !isButtonEnabled);
                    showBlurToast(`✅ 悬浮按钮已${isButtonEnabled ? '关闭' : '开启'}!`, 3000, false);
                    location.reload();
                }
            );
            GM_registerMenuCommand(
                isDynamicObserverEnabled ? '关闭动态检测' : '开启动态检测',
                () => {
                    GM_setValue(BUTTON_STORAGE.DYNAMIC_OBSERVER_ENABLED, !isDynamicObserverEnabled);
                    showBlurToast(`✅ 动态检测已${isDynamicObserverEnabled ? '关闭' : '开启'}!`, 3000, false);
                    location.reload();
                }
            );

            GM_registerMenuCommand('检测CSS隐藏规则', () => checkCssFile(false));
            GM_registerMenuCommand('重置悬浮按钮位置', resetButtonPosition);

            if (isButtonEnabled) {
                document.readyState === 'loading' ?
                    document.addEventListener('DOMContentLoaded', ensureButtonExists) :
                    ensureButtonExists();
            }
        } catch (e) {
            console.error(`[Via CSS Logger] 初始化失败: ${e.message}`);
            showBlurToast(`❌ 脚本初始化失败: ${e.message}`, 5000, false);
        }
    };

    init();
})();