图片链接提取与PDF导出通用版

可自定义父元素选择器,提取指定元素内的图片链接并导出为PDF

// ==UserScript==
// @name         图片链接提取与PDF导出通用版
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  可自定义父元素选择器,提取指定元素内的图片链接并导出为PDF
// @author       TedLife
// @homepageURL  https://tedlife.com/
// @license MIT
// @match        *://*/*
// @grant        GM_setClipboard
// @grant        GM_notification
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
// @resource     pdfCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css
// @connect      *
// ==/UserScript==

(function() {
    'use strict';
    
    // 默认配置
    const defaultConfig = {
        selector: '.EaCvy',
        selectorType: 'class' // 'class', 'id', 'custom'
    };
    
    // 获取或设置配置
    function getConfig() {
        const saved = GM_getValue('imgExtractorConfig', null);
        return saved ? JSON.parse(saved) : defaultConfig;
    }
    
    function saveConfig(config) {
        GM_setValue('imgExtractorConfig', JSON.stringify(config));
    }
    
    // 当前配置
    let currentConfig = getConfig();
    
    // 添加PDF生成所需的Bootstrap样式
    const pdfCSS = GM_getResourceText('pdfCSS');
    GM_addStyle(`
        /* 浮动按钮样式 */
        #pdf-extractor-btn {
            position: fixed;
            bottom: 80px;
            right: 20px;
            z-index: 9999;
            padding: 12px 20px;
            background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
            color: white;
            border: none;
            border-radius: 50px;
            cursor: pointer;
            font-weight: bold;
            box-shadow: 0 6px 20px rgba(37, 117, 252, 0.5);
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        #pdf-extractor-btn:hover {
            transform: translateY(-3px);
            box-shadow: 0 8px 25px rgba(37, 117, 252, 0.7);
        }
        
        #pdf-extractor-btn:active {
            transform: translateY(1px);
        }
        
        #pdf-extractor-btn .spinner {
            display: none;
            width: 16px;
            height: 16px;
            border: 3px solid rgba(255,255,255,0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s linear infinite;
        }
        
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        
        /* 结果面板样式 */
        #pdf-extractor-container {
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 9998;
            background: white;
            border-radius: 15px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.2);
            padding: 20px;
            width: 400px;
            max-height: 70vh;
            overflow: auto;
            display: none;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        #pdf-extractor-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 15px;
            border-bottom: 1px solid #eee;
        }
        
        #pdf-extractor-title {
            margin: 0;
            color: #2c3e50;
            font-size: 1.4rem;
        }
        
        #pdf-extractor-close {
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            color: #777;
            padding: 0 10px;
        }
        
        .image-result-item {
            display: flex;
            padding: 12px;
            border-bottom: 1px solid #f1f1f1;
            transition: background 0.2s;
        }
        
        .image-result-item:hover {
            background: #f9f9ff;
        }
        
        .image-preview {
            width: 70px;
            height: 70px;
            object-fit: cover;
            border-radius: 8px;
            margin-right: 15px;
            border: 1px solid #eee;
            background: #f8f8f8;
        }
        
        .image-details {
            flex: 1;
            overflow: hidden;
        }
        
        .image-url {
            display: block;
            word-break: break-all;
            font-size: 0.85rem;
            color: #3498db;
            text-decoration: none;
            margin-bottom: 8px;
        }
        
        .image-url:hover {
            text-decoration: underline;
        }
        
        .image-actions {
            display: flex;
            gap: 8px;
        }
        
        .action-btn {
            padding: 5px 12px;
            border-radius: 4px;
            font-size: 0.85rem;
            cursor: pointer;
            border: none;
            background: #f1f5ff;
            color: #2575fc;
            transition: all 0.2s;
        }
        
        .action-btn:hover {
            background: #e1ebff;
            transform: translateY(-1px);
        }
        
        .action-btn.copy {
            background: #e8f7f0;
            color: #27ae60;
        }
        
        .action-btn.copy:hover {
            background: #d1f2e5;
        }
        
        .summary-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-top: 15px;
            padding-top: 15px;
            border-top: 1px solid #eee;
        }
        
        .image-count {
            font-weight: bold;
            color: #2c3e50;
        }
        
        #generate-pdf-btn {
            padding: 10px 20px;
            background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%);
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-weight: bold;
            transition: all 0.3s;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        #generate-pdf-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
        }
        
        #generate-pdf-btn .spinner {
            display: none;
            width: 16px;
            height: 16px;
            border: 3px solid rgba(255,255,255,0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s linear infinite;
        }
        
        .no-images {
            text-align: center;
            padding: 30px 0;
            color: #777;
        }
        
        .no-images i {
            font-size: 3rem;
            color: #e0e0e0;
            margin-bottom: 15px;
            display: block;
        }
        
        /* 加载动画 */
        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(255, 255, 255, 0.9);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            z-index: 10000;
            display: none;
        }
        
        .loading-spinner {
            width: 60px;
            height: 60px;
            border: 5px solid #f3f3f3;
            border-top: 5px solid #3498db;
            border-radius: 50%;
            animation: spin 1.5s linear infinite;
            margin-bottom: 20px;
        }
        
        .loading-text {
            font-size: 1.2rem;
            color: #2c3e50;
            font-weight: 500;
        }
        
        .progress-container {
            width: 300px;
            max-width: 80%;
            background: #f1f1f1;
            border-radius: 10px;
            margin-top: 20px;
            overflow: hidden;
        }
        
        .progress-bar {
            height: 20px;
            background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
            border-radius: 10px;
            width: 0%;
            transition: width 0.3s ease;
        }
        
        /* 设置面板样式 */
        #pdf-settings-container {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 10001;
            background: white;
            border-radius: 15px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            padding: 30px;
            width: 500px;
            max-width: 90vw;
            display: none;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        #pdf-settings-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 10000;
            display: none;
        }
        
        .settings-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 25px;
            padding-bottom: 15px;
            border-bottom: 2px solid #f0f0f0;
        }
        
        .settings-title {
            margin: 0;
            color: #2c3e50;
            font-size: 1.5rem;
        }
        
        .settings-close {
            background: none;
            border: none;
            font-size: 28px;
            cursor: pointer;
            color: #777;
            padding: 0 10px;
        }
        
        .form-group {
            margin-bottom: 20px;
        }
        
        .form-label {
            display: block;
            margin-bottom: 8px;
            font-weight: 600;
            color: #2c3e50;
        }
        
        .form-input {
            width: 100%;
            padding: 12px 15px;
            border: 2px solid #e1e8ed;
            border-radius: 8px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        
        .form-input:focus {
            outline: none;
            border-color: #2575fc;
        }
        
        .form-select {
            width: 100%;
            padding: 12px 15px;
            border: 2px solid #e1e8ed;
            border-radius: 8px;
            font-size: 14px;
            background: white;
            cursor: pointer;
        }
        
        .form-help {
            font-size: 12px;
            color: #666;
            margin-top: 5px;
        }
        
        .settings-buttons {
            display: flex;
            gap: 15px;
            justify-content: flex-end;
            margin-top: 30px;
            padding-top: 20px;
            border-top: 1px solid #eee;
        }
        
        .btn-save {
            padding: 12px 25px;
            background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-weight: bold;
            transition: all 0.3s;
        }
        
        .btn-save:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 15px rgba(39, 174, 96, 0.4);
        }
        
        .btn-cancel {
            padding: 12px 25px;
            background: #f8f9fa;
            color: #6c757d;
            border: 2px solid #e9ecef;
            border-radius: 8px;
            cursor: pointer;
            font-weight: bold;
            transition: all 0.3s;
        }
        
        .btn-cancel:hover {
            background: #e9ecef;
            border-color: #dee2e6;
        }
        
        .btn-settings {
            position: fixed;
            bottom: 140px;
            right: 20px;
            z-index: 9999;
            padding: 10px;
            background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
            color: white;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            width: 45px;
            height: 45px;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4);
            transition: all 0.3s ease;
        }
        
        .btn-settings:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 20px rgba(231, 76, 60, 0.6);
        }
    `);
    
    // 创建UI元素
    const container = document.createElement('div');
    container.id = 'pdf-extractor-container';
    container.innerHTML = `
        <div id="pdf-extractor-header">
            <h3 id="pdf-extractor-title">图片链接提取结果</h3>
            <button id="pdf-extractor-close">×</button>
        </div>
        <div id="image-link-results"></div>
        <div class="summary-row">
            <div class="image-count">找到 <span id="image-count">0</span> 张图片</div>
            <button id="generate-pdf-btn">
                <span class="spinner"></span>
                导出为PDF
            </button>
        </div>
    `;
    
    const toggleBtn = document.createElement('button');
    toggleBtn.id = 'pdf-extractor-btn';
    toggleBtn.innerHTML = `
        <span class="spinner"></span>
        <span class="btn-text">提取图片链接</span>
    `;
    
    // 创建设置按钮
    const settingsBtn = document.createElement('button');
    settingsBtn.className = 'btn-settings';
    settingsBtn.innerHTML = '⚙️';
    settingsBtn.title = '设置选择器';
    
    // 创建设置面板
    const settingsOverlay = document.createElement('div');
    settingsOverlay.id = 'pdf-settings-overlay';
    
    const settingsContainer = document.createElement('div');
    settingsContainer.id = 'pdf-settings-container';
    settingsContainer.innerHTML = `
        <div class="settings-header">
            <h3 class="settings-title">选择器设置</h3>
            <button class="settings-close">×</button>
        </div>
        <form id="settings-form">
            <div class="form-group">
                <label class="form-label">选择器类型</label>
                <select class="form-select" id="selector-type">
                    <option value="class">CSS类名 (class)</option>
                    <option value="id">元素ID</option>
                    <option value="custom">自定义选择器</option>
                </select>
                <div class="form-help">选择要匹配的元素类型</div>
            </div>
            <div class="form-group">
                <label class="form-label">选择器值</label>
                <input type="text" class="form-input" id="selector-value" placeholder="例如:EaCvy">
                <div class="form-help" id="selector-help">输入CSS类名(不包含点号)</div>
            </div>
            <div class="form-group">
                <label class="form-label">预览选择器</label>
                <input type="text" class="form-input" id="selector-preview" readonly>
                <div class="form-help">这是最终使用的CSS选择器</div>
            </div>
            <div class="settings-buttons">
                <button type="button" class="btn-cancel">取消</button>
                <button type="submit" class="btn-save">保存设置</button>
            </div>
        </form>
    `;
    
    // 创建加载覆盖层
    const loadingOverlay = document.createElement('div');
    loadingOverlay.className = 'loading-overlay';
    loadingOverlay.innerHTML = `
        <div class="loading-spinner"></div>
        <div class="loading-text">正在生成PDF文件,请稍候...</div>
        <div class="progress-container">
            <div class="progress-bar" id="pdf-progress-bar"></div>
        </div>
        <div id="progress-text">0%</div>
    `;
    
    // 添加到文档
    document.body.appendChild(container);
    document.body.appendChild(toggleBtn);
    document.body.appendChild(settingsBtn);
    document.body.appendChild(settingsOverlay);
    document.body.appendChild(settingsContainer);
    document.body.appendChild(loadingOverlay);
    
    // 设置面板相关函数
    function updateSelectorPreview() {
        const type = document.getElementById('selector-type').value;
        const value = document.getElementById('selector-value').value.trim();
        const preview = document.getElementById('selector-preview');
        const help = document.getElementById('selector-help');
        
        if (!value) {
            preview.value = '';
            return;
        }
        
        switch (type) {
            case 'class':
                preview.value = `.${value} img`;
                help.textContent = '输入CSS类名(不包含点号)';
                break;
            case 'id':
                preview.value = `#${value} img`;
                help.textContent = '输入元素ID(不包含井号)';
                break;
            case 'custom':
                preview.value = `${value} img`;
                help.textContent = '输入完整的CSS选择器';
                break;
        }
    }
    
    function loadSettingsToForm() {
        const config = getConfig();
        document.getElementById('selector-type').value = config.selectorType;
        
        // 从选择器中提取值
        let value = '';
        if (config.selectorType === 'class' && config.selector.startsWith('.')) {
            value = config.selector.replace(/\s+img$/, '').substring(1);
        } else if (config.selectorType === 'id' && config.selector.startsWith('#')) {
            value = config.selector.replace(/\s+img$/, '').substring(1);
        } else if (config.selectorType === 'custom') {
            value = config.selector.replace(/\s+img$/, '');
        }
        
        document.getElementById('selector-value').value = value;
        updateSelectorPreview();
    }
    
    function showSettings() {
        loadSettingsToForm();
        settingsOverlay.style.display = 'block';
        settingsContainer.style.display = 'block';
    }
    
    function hideSettings() {
        settingsOverlay.style.display = 'none';
        settingsContainer.style.display = 'none';
    }
    
    // 事件处理
    toggleBtn.addEventListener('click', function() {
        extractImageLinks();
        container.style.display = container.style.display === 'block' ? 'none' : 'block';
        toggleBtn.querySelector('.btn-text').textContent = 
            container.style.display === 'block' ? '隐藏结果' : '提取图片链接';
    });
    
    document.getElementById('pdf-extractor-close').addEventListener('click', function() {
        container.style.display = 'none';
        toggleBtn.querySelector('.btn-text').textContent = '提取图片链接';
    });
    
    document.getElementById('generate-pdf-btn').addEventListener('click', generatePDF);
    
    // 设置面板事件处理
    settingsBtn.addEventListener('click', showSettings);
    
    settingsOverlay.addEventListener('click', hideSettings);
    
    settingsContainer.addEventListener('click', function(e) {
        e.stopPropagation(); // 防止点击面板内容时关闭
    });
    
    document.querySelector('.settings-close').addEventListener('click', hideSettings);
    
    document.querySelector('.btn-cancel').addEventListener('click', hideSettings);
    
    // 选择器类型和值变化时更新预览
    document.getElementById('selector-type').addEventListener('change', updateSelectorPreview);
    document.getElementById('selector-value').addEventListener('input', updateSelectorPreview);
    
    // 设置表单提交
    document.getElementById('settings-form').addEventListener('submit', function(e) {
        e.preventDefault();
        
        const type = document.getElementById('selector-type').value;
        const value = document.getElementById('selector-value').value.trim();
        
        if (!value) {
            GM_notification({
                title: '设置错误',
                text: '请输入选择器值',
                timeout: 3000
            });
            return;
        }
        
        let selector;
        switch (type) {
            case 'class':
                selector = `.${value}`;
                break;
            case 'id':
                selector = `#${value}`;
                break;
            case 'custom':
                selector = value;
                break;
        }
        
        // 测试选择器是否有效
        try {
            document.querySelectorAll(`${selector} img`);
        } catch (error) {
            GM_notification({
                title: '选择器错误',
                text: '无效的CSS选择器,请检查语法',
                timeout: 3000
            });
            return;
        }
        
        // 保存配置
        currentConfig = {
            selector: selector,
            selectorType: type
        };
        saveConfig(currentConfig);
        
        hideSettings();
        
        GM_notification({
            title: '设置已保存',
            text: `选择器已更新为: ${selector} img`,
            timeout: 3000
        });
     });
     
     // 初始化设置面板显示
     setTimeout(() => {
         loadSettingsToForm();
     }, 100);
     
     // 提取图片链接函数
    function extractImageLinks() {
        const config = getConfig();
        const selector = `${config.selector} img`;
        const elements = document.querySelectorAll(selector);
        const resultsContainer = document.getElementById('image-link-results');
        const countElement = document.getElementById('image-count');
        
        resultsContainer.innerHTML = '';
        
        if (elements.length === 0) {
            resultsContainer.innerHTML = `
                <div class="no-images">
                    <div>🖼️</div>
                    <p>没有找到符合条件的图片</p>
                    <p>当前选择器: <code>${selector}</code></p>
                    <p>点击右下角的⚙️按钮可以修改选择器设置</p>
                </div>
            `;
            countElement.textContent = '0';
            return;
        }
        
        const fragment = document.createDocumentFragment();
        let validImageCount = 0;
        
        elements.forEach(img => {
            // 优先获取data-src(用于懒加载),没有则使用src
            const src = img.dataset.src || img.src;
            
            if (!src) return;
            validImageCount++;
            
            const item = document.createElement('div');
            item.className = 'image-result-item';
            
            const imgPreview = document.createElement('img');
            imgPreview.className = 'image-preview';
            imgPreview.src = src;
            imgPreview.onerror = function() {
                this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="70" height="70" viewBox="0 0 24 24" fill="none" stroke="%23ccc" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>';
            };
            
            const details = document.createElement('div');
            details.className = 'image-details';
            
            const url = document.createElement('a');
            url.className = 'image-url';
            url.href = src;
            url.textContent = src;
            url.target = '_blank';
            
            const actions = document.createElement('div');
            actions.className = 'image-actions';
            
            const copyBtn = document.createElement('button');
            copyBtn.className = 'action-btn copy';
            copyBtn.textContent = '复制链接';
            
            copyBtn.addEventListener('click', () => {
                GM_setClipboard(src);
                GM_notification({
                    text: '图片链接已复制到剪贴板',
                    timeout: 2000
                });
            });
            
            actions.appendChild(copyBtn);
            details.appendChild(url);
            details.appendChild(actions);
            item.appendChild(imgPreview);
            item.appendChild(details);
            fragment.appendChild(item);
        });
        
        resultsContainer.appendChild(fragment);
        countElement.textContent = validImageCount;
        
        // 显示通知
        if (validImageCount > 0) {
            GM_notification({
                title: '图片链接提取完成',
                text: `成功提取了 ${validImageCount} 张图片的链接`,
                timeout: 3000
            });
        }
    }
    
    // 生成PDF函数(优化版)
    function generatePDF() {
        const btn = document.getElementById('generate-pdf-btn');
        const btnSpinner = btn.querySelector('.spinner');
        const config = getConfig();
        const selector = `${config.selector} img`;
        const images = Array.from(document.querySelectorAll(selector));
        const validImages = images.filter(img => img.dataset.src || img.src);
        
        if (validImages.length === 0) {
            GM_notification({
                title: '生成PDF失败',
                text: '没有找到可用的图片',
                timeout: 3000
            });
            return;
        }
        
        // 显示加载状态
        loadingOverlay.style.display = 'flex';
        btnSpinner.style.display = 'inline-block';
        btn.disabled = true;
        
        // 获取进度元素
        const progressBar = document.getElementById('pdf-progress-bar');
        const progressText = document.querySelector('#progress-text');
        
        // 创建PDF文档
        const { jsPDF } = window.jspdf;
        const doc = new jsPDF();
        
        // 存储所有图片的Base64数据
        const imageDataPromises = [];
        let processedCount = 0;
        
        // 获取所有图片的Base64数据
        validImages.forEach(img => {
            const src = img.dataset.src || img.src;
            const promise = new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: src,
                    responseType: "blob",
                    onload: function(response) {
                        const reader = new FileReader();
                        reader.onloadend = function() {
                            processedCount++;
                            const progress = Math.round((processedCount / validImages.length) * 100);
                            progressBar.style.width = `${progress}%`;
                            progressText.textContent = `${progress}%`;
                            
                            resolve({
                                data: reader.result,
                                width: img.naturalWidth || img.width,
                                height: img.naturalHeight || img.height
                            });
                        };
                        reader.readAsDataURL(response.response);
                    },
                    onerror: function() {
                        processedCount++;
                        resolve(null); // 忽略加载失败的图片
                    }
                });
            });
            imageDataPromises.push(promise);
        });
        
        // 等待所有图片加载完成
        Promise.all(imageDataPromises).then(imageDataArray => {
            // 过滤掉加载失败的图片
            const validImageData = imageDataArray.filter(data => data !== null);
            
            if (validImageData.length === 0) {
                loadingOverlay.style.display = 'none';
                btnSpinner.style.display = 'none';
                btn.disabled = false;
                GM_notification({
                    title: '生成PDF失败',
                    text: '所有图片加载失败',
                    timeout: 3000
                });
                return;
            }
            
            // 添加图片到PDF
            validImageData.forEach((imageData, index) => {
                const imgWidth = imageData.width;
                const imgHeight = imageData.height;
                
                // 计算图片在PDF中的尺寸(保持比例)
                const pageWidth = doc.internal.pageSize.getWidth();
                const pageHeight = doc.internal.pageSize.getHeight();
                
                // 计算缩放比例(保留5%的边距)
                const margin = pageWidth * 0.05;
                const maxWidth = pageWidth - (margin * 2);
                const maxHeight = pageHeight - (margin * 2);
                
                let widthRatio = maxWidth / imgWidth;
                let heightRatio = maxHeight / imgHeight;
                let ratio = Math.min(widthRatio, heightRatio);
                
                const scaledWidth = imgWidth * ratio;
                const scaledHeight = imgHeight * ratio;
                
                // 计算居中位置
                const x = (pageWidth - scaledWidth) / 2;
                const y = (pageHeight - scaledHeight) / 2;
                
                // 添加新页面(第一页除外)
                if (index > 0) {
                    doc.addPage();
                }
                
                // 添加图片(不添加任何文字)
                doc.addImage(
                    imageData.data,
                    'JPEG',
                    x,
                    y,
                    scaledWidth,
                    scaledHeight
                );
            });
            
            // 保存PDF
            const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
            doc.save(`extracted-images-${timestamp}.pdf`);
            
            // 隐藏加载状态
            loadingOverlay.style.display = 'none';
            btnSpinner.style.display = 'none';
            btn.disabled = false;
            
            GM_notification({
                title: 'PDF导出成功',
                text: `已导出 ${validImageData.length} 张图片`,
                timeout: 3000
            });
        });
    }
})();