SVG嗅探器

嗅探页面中的SVG资源

// ==UserScript==
// @name         SVG嗅探器
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  嗅探页面中的SVG资源
// @author       YourName
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_notification
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @icon         https://cdn-icons-png.flaticon.com/512/149/149071.png
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 配置参数
    const CONFIG = {
        buttonSize: 30,
        activeColor: '#3498db',
        hoverColor: '#2980b9',
        zIndex: 99999,
        positionOffset: 25,
        touchDelay: 300
    };

    // 添加主样式
    GM_addStyle(`
          /* 按钮容器 */
        .radar-container {
            position: fixed;
            z-index: ${CONFIG.zIndex};
            cursor: move;
            transition: transform 0.2s;
            touch-action: none;
        }
        
        /* 按钮 */
        .radar-button {
            width: ${CONFIG.buttonSize}px;
            height: ${CONFIG.buttonSize}px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            background: linear-gradient(135deg, #3498db, #2c3e50);
            box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3), 
                        0 0 0 4px rgba(255, 255, 255, 0.15),
                        inset 0 0 12px rgba(0, 0, 0, 0.3);
            cursor: pointer;
            border: none;
            outline: none;
            position: relative;
            overflow: hidden;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
            animation: pulse 2s infinite;
            transition: transform 0.3s, box-shadow 0.3s;
        }
        
        .radar-button:hover {
            transform: scale(1.05);
            box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4), 
                        0 0 0 4px rgba(255, 255, 255, 0.25),
                        inset 0 0 15px rgba(0, 0, 0, 0.4);
        }
        
        .radar-button:active {
            transform: scale(0.95);
        }
        
        /* 图标 */
        .radar-icon {
            width: 24px;
            height: 24px;
            position: relative;
            display: flex;
            justify-content: center;
            align-items: center;
            filter: drop-shadow(0 0 2px rgba(255,255,255,0.5));
            animation: radar-scan 4s linear infinite;
        }
        
        .radar-icon svg {
            width: 100%;
            height: 100%;
        }
        
        #svgSnifferModal {
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 90%;
            max-width: 800px;
            max-height: 80vh;
            background: white;
            z-index: 10000;
            border-radius: 12px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
            overflow: hidden;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        .modal-header {
            background: linear-gradient(135deg, #3498db, #2980b9);
            color: white;
            padding: 18px 25px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid rgba(255, 255, 255, 0.2);
        }
        
        .modal-header h2 {
            margin: 0;
            font-size: 1.4rem;
            font-weight: 600;
        }
        
        .close-btn {
            background: none;
            border: none;
            color: white;
            font-size: 1.8rem;
            cursor: pointer;
            width: 36px;
            height: 36px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 0.2s;
        }
        
        .close-btn:hover {
            background: rgba(255, 255, 255, 0.2);
        }
        
        .action-bar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 15px 25px;
            background: #f8f9fa;
            border-bottom: 1px solid #e9ecef;
        }
        
        .select-all-control {
            display: flex;
            align-items: center;
            gap: 10px;
            font-size: 1rem;
        }
        
        .action-buttons {
            display: flex;
            gap: 12px;
        }
        
        .action-btn {
            padding: 10px 20px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 600;
            font-size: 0.95rem;
            transition: all 0.2s;
        }
        
        .download-btn {
            background: #27ae60;
            color: white;
        }
        
        .download-btn:hover {
            background: #219653;
            transform: translateY(-2px);
        }
        
        .copy-btn {
            background: #2980b9;
            color: white;
        }
        
        .copy-btn:hover {
            background: #2573a7;
            transform: translateY(-2px);
        }
        
        .modal-content {
            padding: 20px;
            overflow-y: auto;
            max-height: 60vh;
        }
        
        .svg-item {
            display: flex;
            align-items: center;
            padding: 15px;
            border-bottom: 1px solid #eee;
            transition: background 0.2s;
        }
        
        .svg-item:hover {
            background-color: #f8fafc;
        }
        
        .svg-checkbox {
            margin-right: 20px;
            width: 20px;
            height: 20px;
            cursor: pointer;
        }
        
        .svg-preview {
            width: 50px;
            height: 50px;
            margin-right: 20px;
            border: 1px solid #e0e0e0;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 6px;
            background: white;
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
        }
        
        .svg-preview svg {
            max-width: 100%;
            max-height: 100%;
        }
        
        .svg-name {
            flex-grow: 1;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            font-size: 1.05rem;
            color: #2c3e50;
        }
        
        .overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            z-index: 9999;
            display: none;
        }
        
        .loading {
            text-align: center;
            padding: 30px;
            font-size: 1.2rem;
            color: #666;
        }
        
        .copy-notification {
            position: fixed;
            top: 30px;
            left: 50%;
            transform: translateX(-50%);
            background-color: #27ae60;
            color: white;
            padding: 12px 25px;
            border-radius: 6px;
            z-index: 100000;
            opacity: 0;
            transition: opacity 0.5s;
            pointer-events: none;
            white-space: nowrap;
            font-weight: 500;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        }
        
        @keyframes radar-scan {
            0% {
                transform: rotate(0deg);
            }
            100% {
                transform: rotate(360deg);
            }
        }
        
        @keyframes pulse {
            0% {
                box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.6);
            }
            70% {
                box-shadow: 0 0 0 12px rgba(52, 152, 219, 0);
            }
            100% {
                box-shadow: 0 0 0 0 rgba(52, 152, 219, 0);
            }
        }
    `);

    // 创建按钮容器
    const radarContainer = document.createElement('div');
    radarContainer.className = 'radar-container';
    radarContainer.id = 'radarContainer';
    
    // 创建按钮
    const radarButton = document.createElement('div');
    radarButton.className = 'radar-button';
    radarButton.id = 'radarButton';
    radarButton.innerHTML = `
        <div class="radar-icon">
            <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="white">
                <path d="M512 625.8c-63 0-113.8-51-113.8-113.8s51-113.8 113.8-113.8 113.8 51 113.8 113.8-50.8 113.8-113.8 113.8z m0-165.6c-28.6 0-51.8 23.2-51.8 51.8 0 28.6 23.2 51.8 51.8 51.8 28.6 0 51.8-23.2 51.8-51.8 0-28.6-23.2-51.8-51.8-51.8zM843.2 791.4c-6.6 0-12.8-2-18.6-6.2-13.6-10.4-16.6-29.8-6.2-43.4 50-66.6 76.6-146.2 76.6-229.8s-26.4-163-76.6-229.8c-10.4-13.6-7.4-33.2 6.2-43.4 13.6-10.4 33.2-7.4 43.4 6.2 58.4 77.4 89 169.8 89 267s-30.6 189.6-89 267c-6.2 8.2-15.4 12.4-24.8 12.4zM180.8 791.4c-9.6 0-18.6-4.2-24.8-12.4-58.4-77.4-89-169.8-89-267S97.6 322.4 156 245c10.4-13.6 29.8-16.6 43.4-6.2 13.6 10.4 16.6 29.8 6.2 43.4-50 66.6-76.6 146.2-76.6 229.8s26.4 163 76.6 229.8c10.4 13.6 7.4 33.2-6.2 43.4-5.4 4.2-12 6.2-18.6 6.2zM710.8 692c-6.6 0-12.8-2-18.6-6.2-13.6-10.4-16.6-29.8-6.2-43.4 28.6-37.6 43.4-82.8 43.4-130.4s-15-92.8-43.4-130.4c-10.4-13.6-7.4-33.2 6.2-43.4 13.6-10.4 33.2-7.4 43.4 6.2 36.4 48.8 55.8 106.8 55.8 167.6s-19.4 119.2-55.8 167.6c-6.2 8.4-15.4 12.4-24.8 12.4zM313.4 692c-9.6 0-18.6-4.2-24.8-12.4-36.4-48.8-55.8-106.8-55.8-167.6s19.4-119.2 55.8-167.6c10.4-13.6 29.8-16.6 43.4-6.2 13.6 10.4 16.6 29.8 6.2 43.4-28.6 37.6-43.4 82.8-43.4 130.4s15 92.8 43.4 130.4c10.4 13.6 7.4 33.2-6.2 43.4-5.4 4.2-12 6.2-18.6 6.2z"></path>
            </svg>
        </div>
    `;
    
    radarContainer.appendChild(radarButton);
    document.body.appendChild(radarContainer);

    // 创建SVG嗅探模态框
    const svgModal = document.createElement('div');
    svgModal.id = 'svgSnifferModal';
    svgModal.innerHTML = `
        <div class="modal-header">
            <h2>SVG资源列表</h2>
            <button class="close-btn">&times;</button>
        </div>
        <div class="action-bar">
            <div class="select-all-control">
                <input type="checkbox" id="selectAll">
                <label for="selectAll">全选</label>
            </div>
            <div class="action-buttons">
                <button class="action-btn download-btn">下载选中</button>
                <button class="action-btn copy-btn">复制SVG</button>
            </div>
        </div>
        <div class="modal-content" id="svgList">
            <div class="loading">正在扫描页面SVG资源...</div>
        </div>
    `;
    document.body.appendChild(svgModal);

    // 创建遮罩层
    const overlay = document.createElement('div');
    overlay.className = 'overlay';
    document.body.appendChild(overlay);

    // 创建复制通知
    const copyNotification = document.createElement('div');
    copyNotification.className = 'copy-notification';
    document.body.appendChild(copyNotification);

    // 全局变量
    let globalSvgItems = [];
    let isDragging = false;
    let startX, startY, startLeft, startTop;
    let dragStartTime = 0;
    let touchTimer = null;

    // 初始化按钮
    function initRadarButton() {
        const domain = location.hostname.replace(/\./g, '-');
        const positionKey = `radarPosition_${domain}`;
        
        // 设置初始位置
        const savedPosition = GM_getValue(positionKey);
        if (savedPosition) {
            radarContainer.style.left = `${savedPosition.x}px`;
            radarContainer.style.top = `${savedPosition.y}px`;
        } else {
            radarContainer.style.right = `${CONFIG.positionOffset}px`;
            radarContainer.style.bottom = `${CONFIG.positionOffset}px`;
        }
        
        // 设置拖拽事件
        radarContainer.addEventListener('mousedown', startDrag);
        radarContainer.addEventListener('touchstart', startDrag, { passive: false });
        
        radarButton.addEventListener('click', (e) => {
            if (!isDragging && Date.now() - dragStartTime > CONFIG.touchDelay) {
                showSVGList();
            }
        });
    }

    // 开始拖拽
    function startDrag(e) {
        e.preventDefault();
        
        // 获取初始位置
        const clientX = e.clientX || e.touches[0].clientX;
        const clientY = e.clientY || e.touches[0].clientY;
        
        // 获取当前计算位置
        const computedStyle = window.getComputedStyle(radarContainer);
        startLeft = parseInt(computedStyle.left) || 0;
        startTop = parseInt(computedStyle.top) || 0;
        
        // 如果使用right定位,转换为left定位
        if (computedStyle.right !== 'auto') {
            const rightPos = parseInt(computedStyle.right);
            startLeft = window.innerWidth - rightPos - CONFIG.buttonSize;
            radarContainer.style.right = 'auto';
            radarContainer.style.left = `${startLeft}px`;
        }
        
        startX = clientX;
        startY = clientY;
        dragStartTime = Date.now();
        
        // 对于触摸设备,延迟判定是否为拖动
        if (e.type === 'touchstart') {
            touchTimer = setTimeout(() => {
                isDragging = true;
                radarContainer.style.transition = 'none';
            }, CONFIG.touchDelay);
        } else {
            isDragging = true;
        }
        
        // 添加事件监听
        document.addEventListener('mousemove', drag);
        document.addEventListener('touchmove', drag, { passive: false });
        document.addEventListener('mouseup', endDrag);
        document.addEventListener('touchend', endDrag);
    }

    // 拖拽中
    function drag(e) {
        if (!isDragging) return;
        e.preventDefault();
        
        const clientX = e.clientX || e.touches[0].clientX;
        const clientY = e.clientY || e.touches[0].clientY;
        
        const dx = clientX - startX;
        const dy = clientY - startY;
        
        radarContainer.style.left = `${startLeft + dx}px`;
        radarContainer.style.top = `${startTop + dy}px`;
        radarContainer.style.right = 'auto';
    }

    // 结束拖拽
    function endDrag(e) {
        if (touchTimer) {
            clearTimeout(touchTimer);
            touchTimer = null;
        }
        
        if (!isDragging) {
            if (Date.now() - dragStartTime < CONFIG.touchDelay) {
                showSVGList();
            }
            return;
        }
        
        isDragging = false;
        radarContainer.style.transition = '';
        
        // 移除事件监听
        document.removeEventListener('mousemove', drag);
        document.removeEventListener('touchmove', drag);
        document.removeEventListener('mouseup', endDrag);
        document.removeEventListener('touchend', endDrag);
        
        // 保存位置
        const domain = location.hostname.replace(/\./g, '-');
        const positionKey = `radarPosition_${domain}`;
        const rect = radarContainer.getBoundingClientRect();
        GM_setValue(positionKey, {
            x: rect.left,
            y: rect.top
        });
    }

    // 收集SVG函数
    function collectSVGs() {
        const svgItems = [];
        
        // 收集页面中的SVG元素
        const svgElements = document.querySelectorAll('svg');
        
        svgElements.forEach((svg, index) => {
            // 尝试获取有意义的名称
            let name = `SVG ${index + 1}`;
            let parent = svg.parentElement;
            while (parent) {
                if (parent.querySelector('h1, h2, h3, h4, h5, h6, [class*="title"], [class*="name"]')) {
                    const nameElement = parent.querySelector('h1, h2, h3, h4, h5, h6, [class*="title"], [class*="name"]');
                    if (nameElement) {
                        name = nameElement.textContent.trim() || name;
                        break;
                    }
                }
                parent = parent.parentElement;
            }
            
            // 克隆SVG元素以避免修改原始元素
            const clonedSvg = svg.cloneNode(true);
            
            // 移除干扰属性
            clonedSvg.removeAttribute('onclick');
            clonedSvg.removeAttribute('onmouseover');
            clonedSvg.removeAttribute('onmouseout');
            
            svgItems.push({
                name: name,
                svg: clonedSvg.outerHTML,
                id: `svg-${index}-${Date.now()}`
            });
        });
        
        return svgItems;
    }

    // 显示SVG列表
    function showSVGList() {
        const modal = document.getElementById('svgSnifferModal');
        const svgList = document.getElementById('svgList');
        
        svgList.innerHTML = '<div class="loading">正在扫描页面SVG资源...</div>';
        modal.style.display = 'block';
        overlay.style.display = 'block';
        
        // 使用setTimeout让UI有机会更新
        setTimeout(() => {
            try {
                const svgItems = collectSVGs();
                globalSvgItems = svgItems; // 存储到全局变量
                
                if (svgItems.length === 0) {
                    svgList.innerHTML = '<div class="loading">没有找到SVG资源</div>';
                    return;
                }
                
                svgList.innerHTML = '';
                
                svgItems.forEach(item => {
                    const itemElement = document.createElement('div');
                    itemElement.className = 'svg-item';
                    itemElement.innerHTML = `
                        <input type="checkbox" class="svg-checkbox" data-id="${item.id}" checked>
                        <div class="svg-preview">${item.svg}</div>
                        <div class="svg-name" title="${item.name}">${item.name}</div>
                    `;
                    svgList.appendChild(itemElement);
                });
            } catch (error) {
                console.error('SVG扫描错误:', error);
                svgList.innerHTML = `<div class="loading">错误: ${error.message}</div>`;
            }
        }, 300);
    }

    // 下载选中的SVG
    function downloadSelected() {
        const checkboxes = document.querySelectorAll('.svg-checkbox:checked');
        if (checkboxes.length === 0) {
            GM_notification({
                text: '请至少选择一个SVG!',
                title: 'SVG嗅探器',
                silent: true
            });
            return;
        }
        
        const zip = new JSZip();
        
        checkboxes.forEach(checkbox => {
            const id = checkbox.dataset.id;
            const item = globalSvgItems.find(i => i.id === id);
            if (item) {
                // 清理文件名
                const cleanName = item.name.replace(/[^\w\u4e00-\u9fa5]/g, '_').substring(0, 50);
                zip.file(`${cleanName}.svg`, item.svg);
            }
        });
        
        zip.generateAsync({type: 'blob'}).then(content => {
            saveAs(content, 'svg_collection.zip');
        });
    }

    // 复制选中的SVG代码
    function copySelected() {
        const checkboxes = document.querySelectorAll('.svg-checkbox:checked');
        if (checkboxes.length === 0) {
            GM_notification({
                text: '请至少选择一个SVG!',
                title: 'SVG嗅探器',
                silent: true
            });
            return;
        }
        
        // 构建SVG代码字符串
        let combinedCode = '';
        
        checkboxes.forEach(checkbox => {
            const id = checkbox.dataset.id;
            const item = globalSvgItems.find(i => i.id === id);
            if (item) {
                combinedCode += `${item.svg}\n\n`;
            }
        });
        
        // 尝试复制到剪贴板
        try {
            GM_setClipboard(combinedCode, 'text');
            showCopyNotification(`已复制 ${checkboxes.length} 个SVG代码`);
        } catch (e) {
            // 回退方案
            const textArea = document.createElement('textarea');
            textArea.value = combinedCode;
            document.body.appendChild(textArea);
            textArea.select();
            document.execCommand('copy');
            document.body.removeChild(textArea);
            showCopyNotification(`已复制 ${checkboxes.length} 个SVG代码 (使用回退方法)`);
        }
    }

    // 显示复制通知
    function showCopyNotification(message) {
        copyNotification.textContent = message;
        copyNotification.style.opacity = '1';
        
        setTimeout(() => {
            copyNotification.style.opacity = '0';
        }, 3000);
    }

    // 设置事件监听器
    function setupEventListeners() {
        // 关闭模态框
        document.querySelector('.close-btn').addEventListener('click', () => {
            document.getElementById('svgSnifferModal').style.display = 'none';
            overlay.style.display = 'none';
        });
        
        // 点击遮罩层关闭模态框
        overlay.addEventListener('click', () => {
            document.getElementById('svgSnifferModal').style.display = 'none';
            overlay.style.display = 'none';
        });
        
        // 下载按钮
        document.querySelector('.download-btn').addEventListener('click', downloadSelected);
        
        // 复制按钮
        document.querySelector('.copy-btn').addEventListener('click', copySelected);
        
        // 全选/取消全选
        document.getElementById('selectAll').addEventListener('change', (e) => {
            const checkboxes = document.querySelectorAll('.svg-checkbox');
            checkboxes.forEach(checkbox => {
                checkbox.checked = e.target.checked;
            });
        });
    }

    // 初始化脚本
    function init() {
        initRadarButton();
        setupEventListeners();
        
        // 确保元素已正确添加到DOM
        setTimeout(() => {
            radarButton.style.display = 'flex';
        }, 100);
    }

    // 启动脚本
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();