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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();
})();