批量图片下载器

批量下载页面图片,支持去重、打包ZIP下载,保留原文件名

// ==UserScript==
// @name         批量图片下载器
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  批量下载页面图片,支持去重、打包ZIP下载,保留原文件名
// @author       upsky
// @match        *://*/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// ==/UserScript==

(function() {
    'use strict';

    let isScriptEnabled = false;
    let scannedImages = new Set();
    let imageList = [];
    let panel = null;
    let floatingButton = null;

    // 从URL中提取文件名
    function extractFileName(url) {
        try {
            const urlObj = new URL(url);
            let pathname = urlObj.pathname;

            // 移除查询参数
            pathname = pathname.split('?')[0];

            // 获取文件名部分
            let filename = pathname.split('/').pop();

            // 如果没有文件名或者文件名为空
            if (!filename || filename === '') {
                filename = 'image';
            }

            // 如果没有扩展名,尝试从Content-Type或默认为jpg
            if (!filename.includes('.')) {
                filename += '.jpg';
            }

            // 清理文件名中的特殊字符
            filename = filename.replace(/[<>:"/\\|?*]/g, '_');

            // 限制文件名长度
            if (filename.length > 100) {
                const ext = filename.split('.').pop();
                const name = filename.substring(0, 100 - ext.length - 1);
                filename = name + '.' + ext;
            }

            return filename;
        } catch (error) {
            console.error('提取文件名失败:', error);
            return 'image.jpg';
        }
    }

    // 检查并处理重复文件名
    function getUniqueFileName(filename, existingNames) {
        let uniqueName = filename;
        let counter = 1;

        while (existingNames.has(uniqueName)) {
            const lastDotIndex = filename.lastIndexOf('.');
            if (lastDotIndex > 0) {
                const name = filename.substring(0, lastDotIndex);
                const ext = filename.substring(lastDotIndex);
                uniqueName = `${name}_${counter}${ext}`;
            } else {
                uniqueName = `${filename}_${counter}`;
            }
            counter++;
        }

        existingNames.add(uniqueName);
        return uniqueName;
    }

    // 创建浮动按钮
    function createFloatingButton() {
        const button = document.createElement('div');
        button.id = 'image-downloader-btn';
        button.innerHTML = '📷';
        button.style.cssText = `
            position: fixed !important;
            bottom: 50px !important;
            right: 20px !important;
            width: 50px !important;
            height: 50px !important;
            background: #007bff !important;
            color: white !important;
            border: 2px solid #0056b3 !important;
            border-radius: 50% !important;
            cursor: pointer !important;
            font-size: 20px !important;
            z-index: 999999 !important;
            box-shadow: 0 4px 15px rgba(0,123,255,0.4) !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            transition: all 0.3s ease !important;
            user-select: none !important;
            font-family: Arial, sans-serif !important;
        `;

        button.addEventListener('mouseenter', () => {
            button.style.transform = 'scale(1.1) !important';
            button.style.boxShadow = '0 6px 20px rgba(0,123,255,0.6) !important';
        });

        button.addEventListener('mouseleave', () => {
            button.style.transform = 'scale(1) !important';
            button.style.boxShadow = '0 4px 15px rgba(0,123,255,0.4) !important';
        });

        button.addEventListener('click', togglePanel);

        document.body.appendChild(button);
        return button;
    }

    // 创建主界面
    function createUI() {
        const panelContainer = document.createElement('div');
        panelContainer.id = 'image-downloader-panel';
        panelContainer.style.cssText = `
            position: fixed !important;
            top: 80px !important;
            right: 20px !important;
            width: 400px !important;
            min-height: 70vh !important;
            background: #ffffff !important;
            border: 2px solid #007bff !important;
            border-radius: 12px !important;
            box-shadow: 0 8px 32px rgba(0,0,0,0.3) !important;
            z-index: 999998 !important;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
            display: none !important;
            overflow: hidden !important;
        `;

        panelContainer.innerHTML = `
            <div style="
                background: linear-gradient(135deg, #007bff, #0056b3) !important;
                color: white !important;
                padding: 16px !important;
                font-weight: bold !important;
                text-align: center !important;
                position: relative !important;
                font-size: 16px !important;
            ">
                🖼️ 批量图片下载器 v1.2
                <button id="close-panel" style="
                    position: absolute !important;
                    right: 12px !important;
                    top: 50% !important;
                    transform: translateY(-50%) !important;
                    background: rgba(255,255,255,0.2) !important;
                    border: none !important;
                    color: white !important;
                    font-size: 20px !important;
                    cursor: pointer !important;
                    width: 28px !important;
                    height: 28px !important;
                    border-radius: 50% !important;
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                ">×</button>
            </div>

            <div style="padding: 20px !important; background: white !important;">
                <div style="margin-bottom: 16px !important;">
                    <button id="toggle-script" style="
                        width: 100% !important;
                        padding: 12px !important;
                        background: #28a745 !important;
                        color: white !important;
                        border: none !important;
                        border-radius: 6px !important;
                        cursor: pointer !important;
                        font-size: 14px !important;
                        font-weight: bold !important;
                        margin-bottom: 12px !important;
                        transition: background 0.3s ease !important;
                    ">🔧 启用脚本</button>

                    <div style="display: flex !important; gap: 8px !important;">
                        <button id="scan-images" style="
                            flex: 1 !important;
                            padding: 10px !important;
                            background: #17a2b8 !important;
                            color: white !important;
                            border: none !important;
                            border-radius: 6px !important;
                            cursor: pointer !important;
                            font-size: 13px !important;
                            font-weight: bold !important;
                        " disabled>🔍 扫描图片</button>

                        <button id="clear-images" style="
                            flex: 1 !important;
                            padding: 10px !important;
                            background: #ffc107 !important;
                            color: #212529 !important;
                            border: none !important;
                            border-radius: 6px !important;
                            cursor: pointer !important;
                            font-size: 13px !important;
                            font-weight: bold !important;
                        " disabled>🗑️ 清空</button>
                    </div>
                </div>

                <div style="margin-bottom: 16px !important;">
                    <div style="
                        display: flex !important;
                        justify-content: space-between !important;
                        align-items: center !important;
                        margin-bottom: 12px !important;
                        padding: 8px 12px !important;
                        background: #f8f9fa !important;
                        border-radius: 6px !important;
                    ">
                        <span style="font-size: 14px !important; font-weight: bold !important; color: #495057 !important;">
                            📊 已扫描: <span id="image-count" style="color: #007bff !important;">0</span> 张
                        </span>
                        <button id="select-all" style="
                            padding: 6px 12px !important;
                            background: #6c757d !important;
                            color: white !important;
                            border: none !important;
                            border-radius: 4px !important;
                            cursor: pointer !important;
                            font-size: 12px !important;
                            font-weight: bold !important;
                        " disabled>☑️ 全选</button>
                    </div>

                    <div id="image-list" style="
                        max-height: 300px !important;
                        overflow-y: auto !important;
                        border: 2px solid #e9ecef !important;
                        border-radius: 8px !important;
                        padding: 8px !important;
                        background: #fafafa !important;
                    "></div>
                </div>

                <button id="download-selected" style="
                    width: 100% !important;
                    padding: 14px !important;
                    background: #dc3545 !important;
                    color: white !important;
                    border: none !important;
                    border-radius: 8px !important;
                    cursor: pointer !important;
                    font-size: 15px !important;
                    font-weight: bold !important;
                    transition: background 0.3s ease !important;
                " disabled>📦 下载选中图片</button>

                <div id="progress-container" style="
                    margin-top: 16px !important;
                    display: none !important;
                    padding: 12px !important;
                    background: #f8f9fa !important;
                    border-radius: 8px !important;
                    border: 1px solid #dee2e6 !important;
                ">
                    <div style="margin-bottom: 8px !important; font-size: 13px !important; font-weight: bold !important; color: #495057 !important;" id="progress-text">准备下载...</div>
                    <div style="
                        width: 100% !important;
                        height: 24px !important;
                        background: #e9ecef !important;
                        border-radius: 12px !important;
                        overflow: hidden !important;
                        border: 1px solid #dee2e6 !important;
                    ">
                        <div id="progress-bar" style="
                            height: 100% !important;
                            background: linear-gradient(90deg, #007bff, #0056b3) !important;
                            width: 0% !important;
                            transition: width 0.3s ease !important;
                            border-radius: 12px !important;
                        "></div>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(panelContainer);
        return panelContainer;
    }

    // 切换面板显示
    function togglePanel() {
        if (!panel) return;

        const isVisible = panel.style.display !== 'none';
        panel.style.display = isVisible ? 'none' : 'block';

        if (!isVisible) {
            panel.style.zIndex = '999998';
        }
    }

    // 扫描页面图片
    function scanImages() {
        const images = document.querySelectorAll('img');
        let newImagesCount = 0;

        images.forEach(img => {
            if (img.src && img.src.startsWith('http') && !scannedImages.has(img.src)) {
                // 检查图片尺寸,过滤掉太小的图片
                if (img.naturalWidth > 50 && img.naturalHeight > 50) {
                    scannedImages.add(img.src);

                    // 提取原始文件名
                    const originalFileName = extractFileName(img.src);

                    imageList.push({
                        src: img.src,
                        alt: img.alt || originalFileName,
                        fileName: originalFileName,
                        selected: false
                    });
                    newImagesCount++;
                }
            }
        });

        updateImageList();
        showNotification(newImagesCount > 0 ? `🎉 发现 ${newImagesCount} 张新图片` : '😅 未发现新图片');
    }

    // 更新图片列表显示
    function updateImageList() {
        const imageListElement = document.getElementById('image-list');
        const imageCountElement = document.getElementById('image-count');

        if (!imageListElement || !imageCountElement) return;

        imageCountElement.textContent = imageList.length;
        imageListElement.innerHTML = '';

        if (imageList.length === 0) {
            imageListElement.innerHTML = `
                <div style="
                    text-align: center !important;
                    padding: 40px 20px !important;
                    color: #6c757d !important;
                    font-size: 14px !important;
                ">
                    📷 暂无图片<br>
                    <small style="color: #adb5bd !important;">点击"扫描图片"开始搜索</small>
                </div>
            `;
        } else {
            imageList.forEach((image, index) => {
                const imageItem = document.createElement('div');
                imageItem.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    padding: 10px !important;
                    margin-bottom: 8px !important;
                    border: 1px solid ${image.selected ? '#007bff' : '#e9ecef'} !important;
                    border-radius: 8px !important;
                    background: ${image.selected ? '#e3f2fd' : '#ffffff'} !important;
                    transition: all 0.2s ease !important;
                    cursor: pointer !important;
                `;

                imageItem.innerHTML = `
                    <input type="checkbox" ${image.selected ? 'checked' : ''}
                           style="
                               margin-right: 12px !important;
                               transform: scale(1.2) !important;
                               cursor: pointer !important;
                           ">
                    <img src="${image.src}" style="
                        width: 50px !important;
                        height: 50px !important;
                        object-fit: cover !important;
                        border-radius: 6px !important;
                        margin-right: 12px !important;
                        border: 2px solid #e9ecef !important;
                    " onerror="this.style.display='none'">
                    <div style="flex: 1 !important; font-size: 12px !important; overflow: hidden !important;">
                        <div style="font-weight: bold !important; margin-bottom: 4px !important; color: #495057 !important;">
                            📄 ${image.fileName}
                        </div>
                        <div style="color: #28a745 !important; font-size: 11px !important; margin-bottom: 2px !important;">
                            原文件名: ${image.fileName}
                        </div>
                        <div style="color: #6c757d !important; word-break: break-all !important; font-size: 10px !important;">
                            ${image.src.length > 50 ? image.src.substring(0, 50) + '...' : image.src}
                        </div>
                    </div>
                `;

                // 点击整个项目来切换选择状态
                imageItem.addEventListener('click', () => {
                    imageList[index].selected = !imageList[index].selected;
                    updateImageList();
                });

                imageListElement.appendChild(imageItem);
            });
        }

        updateButtonStates();
    }

    // 更新按钮状态
    function updateButtonStates() {
        const hasImages = imageList.length > 0;
        const hasSelected = imageList.some(img => img.selected);
        const allSelected = imageList.length > 0 && imageList.every(img => img.selected);

        const clearBtn = document.getElementById('clear-images');
        const selectAllBtn = document.getElementById('select-all');
        const downloadBtn = document.getElementById('download-selected');

        if (clearBtn) clearBtn.disabled = !hasImages;
        if (selectAllBtn) {
            selectAllBtn.disabled = !hasImages;
            selectAllBtn.textContent = allSelected ? '❌ 取消全选' : '☑️ 全选';
        }
        if (downloadBtn) downloadBtn.disabled = !hasSelected;
    }

    // 全选/取消全选
    function toggleSelectAll() {
        const allSelected = imageList.every(img => img.selected);
        imageList.forEach(img => {
            img.selected = !allSelected;
        });
        updateImageList();
    }

    // 下载选中的图片
    async function downloadSelectedImages() {
        const selectedImages = imageList.filter(img => img.selected);

        if (selectedImages.length === 0) {
            showNotification('⚠️ 请先选择要下载的图片');
            return;
        }

        const progressContainer = document.getElementById('progress-container');
        const progressBar = document.getElementById('progress-bar');
        const progressText = document.getElementById('progress-text');

        if (!progressContainer || !progressBar || !progressText) return;

        progressContainer.style.display = 'block';
        progressText.textContent = '🚀 准备下载...';
        progressBar.style.width = '0%';

        const zip = new JSZip();
        const usedFileNames = new Set();
        let completed = 0;

        try {
            for (let i = 0; i < selectedImages.length; i++) {
                const image = selectedImages[i];
                progressText.textContent = `📥 下载图片 ${i + 1}/${selectedImages.length} - ${image.fileName}`;

                try {
                    const response = await fetch(image.src);
                    const blob = await response.blob();

                    // 使用原始文件名,如果重复则添加序号
                    const uniqueFileName = getUniqueFileName(image.fileName, usedFileNames);

                    zip.file(uniqueFileName, blob);
                    completed++;

                    const progress = (completed / selectedImages.length) * 80;
                    progressBar.style.width = progress + '%';
                } catch (error) {
                    console.error('下载图片失败:', image.src, error);
                    // 即使下载失败也要添加文件名到已使用列表,避免序号混乱
                    getUniqueFileName(image.fileName, usedFileNames);
                }
            }

            progressText.textContent = '📦 正在打包ZIP文件...';
            progressBar.style.width = '90%';

            const zipBlob = await zip.generateAsync({
                type: 'blob',
                compression: 'DEFLATE',
                compressionOptions: {
                    level: 6
                }
            });

            progressText.textContent = '✅ 下载完成!';
            progressBar.style.width = '100%';

            // 创建下载链接
            const link = document.createElement('a');
            link.href = URL.createObjectURL(zipBlob);
            link.download = `images_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.zip`;
            link.click();

            setTimeout(() => {
                progressContainer.style.display = 'none';
                URL.revokeObjectURL(link.href);
            }, 3000);

            showNotification(`🎉 成功下载 ${completed} 张图片,保留原文件名`);

        } catch (error) {
            console.error('打包失败:', error);
            showNotification('❌ 下载失败,请重试');
            progressContainer.style.display = 'none';
        }
    }

    // 显示通知
    function showNotification(message) {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed !important;
            top: 80px !important;
            right: 430px !important;
            background: #333 !important;
            color: white !important;
            padding: 12px 16px !important;
            border-radius: 8px !important;
            z-index: 999999 !important;
            font-size: 14px !important;
            font-weight: bold !important;
            max-width: 300px !important;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        `;
        notification.textContent = message;

        document.body.appendChild(notification);

        setTimeout(() => {
            if (notification.parentNode) {
                notification.parentNode.removeChild(notification);
            }
        }, 4000);
    }

    // 绑定事件
    function bindEvents() {
        // 关闭面板
        const closeBtn = document.getElementById('close-panel');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => {
                panel.style.display = 'none';
            });
        }

        // 启用/禁用脚本
        const toggleBtn = document.getElementById('toggle-script');
        if (toggleBtn) {
            toggleBtn.addEventListener('click', () => {
                isScriptEnabled = !isScriptEnabled;
                const scanButton = document.getElementById('scan-images');

                if (isScriptEnabled) {
                    toggleBtn.textContent = '🔧 禁用脚本';
                    toggleBtn.style.background = '#dc3545';
                    if (scanButton) scanButton.disabled = false;
                    showNotification('✅ 脚本已启用');
                } else {
                    toggleBtn.textContent = '🔧 启用脚本';
                    toggleBtn.style.background = '#28a745';
                    if (scanButton) scanButton.disabled = true;
                    showNotification('⏸️ 脚本已禁用');
                }
            });
        }

        // 扫描图片
        const scanBtn = document.getElementById('scan-images');
        if (scanBtn) {
            scanBtn.addEventListener('click', scanImages);
        }

        // 清空列表
        const clearBtn = document.getElementById('clear-images');
        if (clearBtn) {
            clearBtn.addEventListener('click', () => {
                imageList = [];
                scannedImages.clear();
                updateImageList();
                showNotification('🗑️ 已清空图片列表');
            });
        }

        // 全选
        const selectAllBtn = document.getElementById('select-all');
        if (selectAllBtn) {
            selectAllBtn.addEventListener('click', toggleSelectAll);
        }

        // 下载选中图片
        const downloadBtn = document.getElementById('download-selected');
        if (downloadBtn) {
            downloadBtn.addEventListener('click', downloadSelectedImages);
        }
    }

    // 初始化
    function init() {
        setTimeout(() => {
            floatingButton = createFloatingButton();
            panel = createUI();
            bindEvents();

            console.log('🎉 批量图片下载器 v1.2 已加载');

            setTimeout(() => {
                showNotification('🎉 图片下载器已就绪!支持保留原文件名');
            }, 1000);
        }, 1000);
    }

    // 确保在页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();