Greasy Fork 还支持 简体中文。

图片下载器 (修复版)

支持进度显示和选择序号的图片批量下载工具(修复 zip 损坏问题)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         图片下载器 (修复版)
// @namespace    http://tampermonkey.net/
// @version      3.3
// @description  支持进度显示和选择序号的图片批量下载工具(修复 zip 损坏问题)
// @author       陈粥子
// @license      MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_download
// @connect      *
// @run-at       document-end
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
/*                         
            ■■         ■  
 ■■■■■■■■   ■■    ■■■■■■■ 
      ■■    ■■         ■■ 
      ■■ ■  ■■  ■■     ■■ 
      ■■ ■■ ■■  ■      ■■ 
      ■■  ■ ■■ ■■      ■■ 
      ■■  ■ ■■ ■       ■■ 
  ■■■■■■  ■ ■■ ■   ■■■■■■ 
  ■■  ■■    ■■    ■■   ■  
  ■■        ■■  ■ ■■      
  ■■     ■■■■■■■■■■■      
  ■         ■■    ■■      
 ■■   ■■   ■■■    ■■   ■■ 
 ■■■■■■■■  ■■■■   ■■■■■■■ 
      ■■   ■■■ ■■      ■■ 
      ■■  ■■■■ ■■      ■■ 
      ■■  ■ ■■  ■■     ■■ 
      ■■  ■ ■■  ■      ■  
      ■■ ■  ■■         ■  
      ■■■   ■■        ■■  
      ■■    ■■        ■■  
     ■■     ■■        ■■  
  ■■■■■     ■■     ■■■■   
    ■■       ■      ■■    
*/
// ==/UserScript==

(function() {
    'use strict';

    // 全局变量
    let imageUrls = new Set();
    let selectedItems = [];
    const isMobile = window.innerWidth <= 768;

    /* -------------------- UI 创建 & 样式(与之前相同) -------------------- */
    function createUI() {
        const downloadBtn = document.createElement('div');
        downloadBtn.id = 'img-dl-btn';
        downloadBtn.innerHTML = '↓';
        downloadBtn.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 2147483647;
            width: ${isMobile ? '40px' : '50px'};
            height: ${isMobile ? '40px' : '50px'};
            background: #2196F3;
            color: white;
            font-size: ${isMobile ? '20px' : '24px'};
            font-weight: bold;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 50%;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
            cursor: pointer;
            user-select: none;
        `;

        const modal = document.createElement('div');
        modal.id = 'img-dl-modal';
        modal.innerHTML = `
            <div class="modal-header">
                <span>图片预览</span>
                <span class="close-btn">×</span>
            </div>
            <div class="image-grid"></div>
            <div class="modal-footer">
                <button class="select-btn">全选</button>
                <span class="count">0张</span>
                <button class="download-btn" disabled>下载</button>
            </div>
        `;
        modal.style.cssText = `
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 2147483646;
            width: ${isMobile ? '95vw' : '80vw'};
            height: ${isMobile ? '85vh' : '75vh'};
            background: white;
            border-radius: ${isMobile ? '8px' : '12px'};
            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
            flex-direction: column;
            overflow: hidden;
        `;

        const progressModal = document.createElement('div');
        progressModal.id = 'progress-modal';
        progressModal.innerHTML = `
            <div class="progress-header">
                <span>下载进度</span>
            </div>
            <div class="progress-container">
                <div class="progress-bar">
                    <div class="progress-fill"></div>
                </div>
                <div class="progress-text">准备开始下载...</div>
                <div class="progress-stats">
                    <span class="success-count">成功: 0</span>
                    <span class="fail-count">失败: 0</span>
                </div>
            </div>
            <div class="progress-footer">
                <button class="cancel-btn">取消</button>
            </div>
        `;
        progressModal.style.cssText = `
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 2147483646;
            width: ${isMobile ? '80vw' : '400px'};
            background: white;
            border-radius: ${isMobile ? '8px' : '12px'};
            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
            flex-direction: column;
            overflow: hidden;
        `;

        const overlay = document.createElement('div');
        overlay.id = 'img-dl-overlay';
        overlay.style.cssText = `
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.6);
            z-index: 2147483645;
        `;

        document.body.appendChild(downloadBtn);
        document.body.appendChild(modal);
        document.body.appendChild(progressModal);
        document.body.appendChild(overlay);
    }

    function addStyles() {
        const css = `
            #img-dl-modal { display:flex; flex-direction: column; }
            #img-dl-modal .modal-header { padding: 12px 16px; border-bottom: 1px solid #eee; display:flex; justify-content:space-between; align-items:center; font-size:${isMobile?'16px':'18px'}; background:#f8f8f8; flex-shrink:0; }
            #img-dl-modal .close-btn { font-size:24px; cursor:pointer; width:30px; height:30px; display:flex; align-items:center; justify-content:center; border-radius:50%; }
            #img-dl-modal .image-grid { flex:1; padding:10px; overflow-y:auto; display:grid; grid-template-columns:repeat(auto-fill, minmax(${isMobile?'100px':'120px'}, 1fr)); grid-auto-rows:min-content; gap:12px; background:#fff; }
            .image-item { position:relative; border:2px solid #eee; border-radius:8px; overflow:hidden; aspect-ratio:1/1; background:#f5f5f5; display:flex; align-items:center; justify-content:center; box-sizing:border-box; }
            .image-item.selected { border-color:#2196F3; box-shadow:0 0 0 2px rgba(33,150,243,0.3); }
            .image-item img { max-width:100%; max-height:100%; object-fit:contain; display:block; background:#fff; padding:4px; box-sizing:border-box; }
            #img-dl-modal .modal-footer { padding:12px 16px; border-top:1px solid #eee; display:flex; justify-content:space-between; align-items:center; background:#f8f8f8; flex-shrink:0; }
            #img-dl-modal button { padding:8px 16px; border:none; border-radius:4px; background:#2196F3; color:white; cursor:pointer; font-size:${isMobile?'14px':'16px'}; display:flex; align-items:center; justify-content:center; }
            #img-dl-modal .refresh-btn { background:#4CAF50; margin-left:8px; }
            #img-dl-modal .download-btn:disabled { background:#ccc; cursor:not-allowed; }
            #progress-modal { display:flex; flex-direction:column; }
            #progress-modal .progress-header { padding:16px; background:#2196F3; color:white; font-size:18px; text-align:center; }
            #progress-modal .progress-container { padding:20px; }
            #progress-modal .progress-bar { height:20px; background:#eee; border-radius:10px; overflow:hidden; margin-bottom:10px; }
            #progress-modal .progress-fill { height:100%; background:#4CAF50; width:0%; transition:width 0.3s; }
            #progress-modal .progress-text { text-align:center; margin-bottom:10px; font-size:14px; color:#555; }
            #progress-modal .progress-stats { display:flex; justify-content:space-around; font-size:14px; color:#555; }
            #progress-modal .progress-footer { padding:16px; display:flex; justify-content:center; border-top:1px solid #eee; }
            #progress-modal .cancel-btn { padding:8px 24px; background:#f44336; color:white; border:none; border-radius:4px; cursor:pointer; font-size:16px; }
            .image-index { position:absolute; top:0; right:0; background:rgba(33,150,243,0.85); color:white; font-size:14px; width:22px; height:22px; border-radius:0 0 0 12px; display:flex; align-items:center; justify-content:center; font-weight:bold; z-index:1; }
            @media (max-width:480px) {
                #img-dl-modal .image-grid { grid-template-columns:repeat(auto-fill, minmax(80px, 1fr)); gap:10px; padding:8px; }
                .image-item { border-radius:6px; border-width:1px; }
                #img-dl-modal .modal-footer { flex-wrap:wrap; gap:8px; }
                #img-dl-modal .count { order:3; width:100%; text-align:center; }
                #progress-modal { width:90vw; }
                .image-index { width:18px; height:18px; font-size:10px; border-radius:0 0 0 8px; }
                #img-dl-modal button { padding:6px 12px; font-size:13px; }
            }
        `;
        GM_addStyle(css);
    }

    /* -------------------- 获取页面图片(保持原逻辑) -------------------- */
    function getPageImages() {
        const images = new Set();

        document.querySelectorAll('img').forEach(img => {
            if (!img) return;
            const srcs = new Set([
                img.src,
                img.dataset.src,
                img.dataset.original,
                img.dataset.srcset,
                img.dataset.lazySrc,
                img.dataset.lazyload,
                img.dataset.thumb,
                img.getAttribute('data-src'),
                img.getAttribute('data-original'),
                img.getAttribute('data-srcset'),
                img.getAttribute('data-lazy'),
                img.getAttribute('data-lazy-src')
            ].filter(Boolean));

            srcs.forEach(src => {
                if (src.includes(',')) {
                    src.split(',').forEach(part => {
                        const url = part.trim().split(' ')[0];
                        if (url) images.add(cleanImageUrl(url));
                    });
                } else {
                    images.add(cleanImageUrl(src));
                }
            });
        });

        document.querySelectorAll('picture source').forEach(source => {
            if (!source) return;
            const srcset = source.srcset;
            if (!srcset) return;
            srcset.split(',').forEach(part => {
                const url = part.trim().split(' ')[0];
                if (url) images.add(cleanImageUrl(url));
            });
        });

        document.querySelectorAll('*').forEach(el => {
            try {
                const style = window.getComputedStyle(el);
                const bgImages = [];
                if (style.backgroundImage && style.backgroundImage !== 'none') bgImages.push(style.backgroundImage);
                if (style.background && style.background !== 'none') bgImages.push(style.background);
                bgImages.forEach(bg => {
                    const urls = bg.match(/url\(['"]?(.*?)['"]?\)/g);
                    if (urls) {
                        urls.forEach(u => {
                            const clean = u.replace(/^url\(['"]?/, '').replace(/['"]?\)$/, '');
                            images.add(cleanImageUrl(clean));
                        });
                    }
                    const imageSetMatches = bg.match(/image-set\((.*?)\)/g);
                    if (imageSetMatches) {
                        imageSetMatches.forEach(set => {
                            const items = set.match(/url\(['"]?(.*?)['"]?\)/g);
                            if (items) {
                                items.forEach(u => {
                                    const clean = u.replace(/^url\(['"]?/, '').replace(/['"]?\)$/, '');
                                    images.add(cleanImageUrl(clean));
                                });
                            }
                        });
                    }
                });
            } catch (e) {}
        });

        document.querySelectorAll('canvas').forEach(canvas => {
            try {
                const dataURL = canvas.toDataURL('image/png');
                images.add(dataURL);
            } catch (e) {}
        });

        document.querySelectorAll('svg').forEach(svg => {
            try {
                const serializer = new XMLSerializer();
                const svgStr = serializer.serializeToString(svg);
                const blob = new Blob([svgStr], { type: 'image/svg+xml' });
                const url = URL.createObjectURL(blob);
                images.add(url);
            } catch (e) {}
        });

        document.querySelectorAll('video').forEach(video => {
            if (video.poster) images.add(cleanImageUrl(video.poster));
        });

        return Array.from(images);
    }

    function cleanImageUrl(url) {
        if (!url) return '';
        const [baseUrl] = url.split(/[?#]/);
        if (baseUrl.startsWith('//')) return location.protocol + baseUrl;
        if (baseUrl.startsWith('/')) return location.origin + baseUrl;
        if (baseUrl.startsWith('./') || baseUrl.startsWith('../')) return new URL(baseUrl, location.href).href;
        return baseUrl;
    }

    /* -------------------- —— 关键:扩展名相关函数(替换/修复版) —— -------------------- */

    // 1. 仅从 URL 提取扩展名(回退使用)
    function getImageExtension(url) {
        try {
            let cleanUrl = (url || '').split('?')[0].split('#')[0];
            const match = cleanUrl.match(/\.([a-z0-9]{2,5})$/i);
            if (match) {
                const ext = match[1].toLowerCase();
                if (['jpg','jpeg','png','gif','webp','bmp','svg','ico'].includes(ext)) {
                    return ext === 'jpeg' ? 'jpg' : ext;
                }
            }
            return 'jpg';
        } catch (e) {
            return 'jpg';
        }
    }

    // 2. 根据远程响应判断扩展名(支持 GM_xmlhttpRequest 返回的 arraybuffer)
    async function getImageExtensionFromResponse(respObj, url) {
        try {
            // 先从响应头中找 Content-Type(respObj.headers 可能是字符串)
            const headersStr = respObj.responseHeaders || respObj.headers || '';
            const ctMatch = headersStr.match(/content-type:\s*([^\r\n;]+)/i);
            if (ctMatch) {
                const ct = ctMatch[1].toLowerCase();
                if (ct.includes('jpeg') || ct.includes('jpg')) return 'jpg';
                if (ct.includes('png')) return 'png';
                if (ct.includes('gif')) return 'gif';
                if (ct.includes('webp')) return 'webp';
                if (ct.includes('bmp')) return 'bmp';
                if (ct.includes('svg')) return 'svg';
                if (ct.includes('x-icon') || ct.includes('ico')) return 'ico';
            }

            // 再用前几个字节判断(respObj.arrayBuffer 是 ArrayBuffer)
            const arrayBuffer = respObj.arrayBuffer;
            if (arrayBuffer && arrayBuffer.byteLength > 0) {
                const bytes = new Uint8Array(arrayBuffer.slice(0, 12));
                // png
                if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) return 'png';
                // jpg
                if (bytes[0] === 0xFF && bytes[1] === 0xD8) return 'jpg';
                // gif
                if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) return 'gif';
                // webp (RIFF....WEBP)
                if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {
                    const sub = String.fromCharCode(bytes[8], bytes[9], bytes[10], bytes[11]);
                    if (sub === 'WEBP') return 'webp';
                }
                // bmp
                if (bytes[0] === 0x42 && bytes[1] === 0x4D) return 'bmp';
                // svg 文件通常是文本 "<svg"
                const textStart = new TextDecoder().decode(bytes);
                if (textStart.trim().startsWith('<svg')) return 'svg';
            }

            // 最后回退到 URL
            return getImageExtension(url);
        } catch (e) {
            return getImageExtension(url);
        }
    }

    // 3. 正确把图片内容加入 zip(使用 ArrayBuffer)
    async function addImageToZip(zip, respObj, filename) {
        // respObj.arrayBuffer 必须存在(fetchImage 会确保)
        const arrayBuffer = respObj.arrayBuffer;
        if (!arrayBuffer) throw new Error('没有可用的 ArrayBuffer 数据');
        zip.file(filename, arrayBuffer);
    }

    /* -------------------- 网络请求:使用 GM_xmlhttpRequest,返回 arraybuffer(改良) -------------------- */
    function fetchImage(url, timeout = 20000) {
        return new Promise((resolve, reject) => {
            try {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    responseType: 'arraybuffer', // 直接拿到 ArrayBuffer,便于 zip 使用
                    headers: {
                        'Referer': location.href,
                        'Origin': location.origin
                    },
                    timeout: timeout,
                    onload: function(resp) {
                        if (resp.status >= 400) return reject(new Error(`HTTP ${resp.status}`));
                        resolve({
                            arrayBuffer: resp.response, // ArrayBuffer
                            responseHeaders: resp.responseHeaders || '',
                            finalUrl: resp.finalUrl || url,
                            url: url
                        });
                    },
                    onerror: function(err) { reject(new Error('网络错误')); },
                    ontimeout: function() { reject(new Error('请求超时')); },
                    onabort: function() { reject(new Error('请求中止')); }
                });
            } catch (e) {
                reject(e);
            }
        });
    }

    /* -------------------- 预览 / 选择 / 下载 主流程(使用上面修复的函数) -------------------- */

    function showPreview() {
        const modal = document.getElementById('img-dl-modal');
        const grid = modal.querySelector('.image-grid');
        const images = getPageImages();

        imageUrls = new Set(images);
        grid.innerHTML = '';
        selectedItems = [];

        images.forEach((url, index) => {
            const item = document.createElement('div');
            item.className = 'image-item';

            const img = document.createElement('img');
            img.src = url;
            img.alt = `Image ${index + 1}`;
            img.onerror = function() { item.style.display = 'none'; };

            const indexElement = document.createElement('div');
            indexElement.className = 'image-index';
            indexElement.style.display = 'none';
            indexElement.textContent = '0';

            item.appendChild(img);
            item.appendChild(indexElement);
            item.dataset.url = url;

            item.addEventListener('click', () => {
                toggleSelection(item);
                updateSelectionCount();
            });

            grid.appendChild(item);
        });

        modal.style.display = 'flex';
        document.getElementById('img-dl-overlay').style.display = 'block';
        updateSelectionCount();
    }

    function refreshPreview() {
        const modal = document.getElementById('img-dl-modal');
        const grid = modal.querySelector('.image-grid');
        selectedItems = [];
        document.querySelector('.select-btn').textContent = '全选';

        const images = getPageImages();
        imageUrls = new Set(images);
        grid.innerHTML = '';

        images.forEach((url, index) => {
            const item = document.createElement('div');
            item.className = 'image-item';

            const img = document.createElement('img');
            img.src = url;
            img.alt = `Image ${index + 1}`;
            img.onerror = function() { item.style.display = 'none'; };

            const indexElement = document.createElement('div');
            indexElement.className = 'image-index';
            indexElement.style.display = 'none';
            indexElement.textContent = '0';

            item.appendChild(img);
            item.appendChild(indexElement);
            item.dataset.url = url;

            item.addEventListener('click', () => {
                toggleSelection(item);
                updateSelectionCount();
            });

            grid.appendChild(item);
        });

        updateSelectionCount();
    }

    function toggleSelection(item) {
        const isSelected = item.classList.contains('selected');

        if (isSelected) {
            item.classList.remove('selected');
            item.querySelector('.image-index').style.display = 'none';
            const index = selectedItems.indexOf(item);
            if (index !== -1) selectedItems.splice(index, 1);
        } else {
            item.classList.add('selected');
            const indexTag = item.querySelector('.image-index');
            const selectionNumber = selectedItems.length + 1;
            indexTag.textContent = selectionNumber;
            indexTag.style.display = 'block';
            selectedItems.push(item);
        }

        updateSelectedIndexes();
    }

    function updateSelectedIndexes() {
        selectedItems.forEach((item, index) => {
            const indexTag = item.querySelector('.image-index');
            indexTag.textContent = index + 1;
            indexTag.style.display = 'block';
        });
    }

    function updateSelectionCount() {
        const modal = document.getElementById('img-dl-modal');
        modal.querySelector('.count').textContent = `${selectedItems.length}张`;
        modal.querySelector('.download-btn').disabled = selectedItems.length === 0;
    }

    function showProgress(total) {
        const progressModal = document.getElementById('progress-modal');
        progressModal.style.display = 'block';
        document.getElementById('img-dl-overlay').style.display = 'block';
        updateProgress(0, 0, 0, total, '准备开始下载...');
    }

    function updateProgress(current, success, fail, total, message) {
        const progressModal = document.getElementById('progress-modal');
        const progressFill = progressModal.querySelector('.progress-fill');
        const progressText = progressModal.querySelector('.progress-text');
        const successCount = progressModal.querySelector('.success-count');
        const failCount = progressModal.querySelector('.fail-count');

        const percent = total > 0 ? Math.round((current / total) * 100) : 0;
        progressFill.style.width = `${percent}%`;
        progressText.textContent = message || `正在下载 ${current}/${total}...`;
        successCount.textContent = `成功: ${success}`;
        failCount.textContent = `失败: ${fail}`;
    }

    function hideProgress() {
        const progressModal = document.getElementById('progress-modal');
        progressModal.style.display = 'none';
        document.getElementById('img-dl-overlay').style.display = 'none';
    }

    // 核心:批量下载并压缩(使用修复后的 fetch/getExt/addToZip)
    async function downloadSelected() {
        const modal = document.getElementById('img-dl-modal');

        if (selectedItems.length === 0) return;

        modal.querySelector('.download-btn').disabled = true;
        showProgress(selectedItems.length);

        const zip = new JSZip();
        let successCount = 0;
        let failCount = 0;
        let isCancelled = false;

        const cancelBtn = document.querySelector('#progress-modal .cancel-btn');
        const onCancel = () => { isCancelled = true; hideProgress(); modal.querySelector('.download-btn').disabled = false; };
        cancelBtn.addEventListener('click', onCancel, { once: true });

        for (let i = 0; i < selectedItems.length; i++) {
            if (isCancelled) {
                updateProgress(i, successCount, failCount, selectedItems.length, '下载已取消');
                setTimeout(() => hideProgress(), 1200);
                return;
            }

            const url = selectedItems[i].dataset.url;
            updateProgress(i, successCount, failCount, selectedItems.length, `正在请求: ${url.split('/').pop()}`);

            try {
                const respObj = await fetchImage(url);
                const ext = await getImageExtensionFromResponse(respObj, url);
                await addImageToZip(zip, respObj, `image_${i + 1}.${ext}`);
                successCount++;
                updateProgress(i + 1, successCount, failCount, selectedItems.length);
            } catch (err) {
                console.error('下载或压缩单张失败:', url, err);
                failCount++;
                updateProgress(i + 1, successCount, failCount, selectedItems.length, `下载失败: ${url.split('/').pop()}`);
            }

            // 小间隔避免阻塞
            await new Promise(r => setTimeout(r, 100));
        }

        if (successCount > 0 && !isCancelled) {
            updateProgress(selectedItems.length, successCount, failCount, selectedItems.length, '正在生成压缩包...');
            await new Promise(r => setTimeout(r, 300));
            try {
                const content = await zip.generateAsync({ type: 'blob' });
                const blobUrl = URL.createObjectURL(content);
                const a = document.createElement('a');
                a.href = blobUrl;
                a.download = `images_${Date.now()}.zip`;
                a.click();
                URL.revokeObjectURL(blobUrl);
                updateProgress(selectedItems.length, successCount, failCount, selectedItems.length, '下载完成!');
                setTimeout(() => hideProgress(), 1200);
            } catch (e) {
                console.error('生成 zip 失败', e);
                updateProgress(selectedItems.length, successCount, failCount, selectedItems.length, '生成压缩包失败');
                setTimeout(() => hideProgress(), 1200);
            }
        } else if (!isCancelled) {
            updateProgress(selectedItems.length, successCount, failCount, selectedItems.length, '没有成功下载任何图片');
            setTimeout(() => hideProgress(), 1200);
        }

        closeModal();
        modal.querySelector('.download-btn').disabled = false;
    }

    function closeModal() {
        document.getElementById('img-dl-modal').style.display = 'none';
        document.getElementById('img-dl-overlay').style.display = 'none';
    }

    function addRefreshButtonToFooter() {
        const footer = document.querySelector('.modal-footer');
        const refreshBtn = document.createElement('button');
        refreshBtn.className = 'refresh-btn';
        refreshBtn.innerHTML = '刷新';
        refreshBtn.style.cssText = `background:#4CAF50;margin-left:8px;`;
        footer.insertBefore(refreshBtn, footer.querySelector('.count'));
        refreshBtn.addEventListener('click', refreshPreview);
    }

    function init() {
        createUI();
        addStyles();
        addRefreshButtonToFooter();

        document.getElementById('img-dl-btn').addEventListener('click', showPreview);
        document.querySelector('#img-dl-modal .close-btn').addEventListener('click', closeModal);

        document.querySelector('#img-dl-modal .select-btn').addEventListener('click', function() {
            const items = document.querySelectorAll('.image-item');
            const allSelected = items.length > 0 && Array.from(items).every(item => item.classList.contains('selected'));

            selectedItems.forEach(item => { item.classList.remove('selected'); item.querySelector('.image-index').style.display = 'none'; });
            selectedItems = [];

            items.forEach(item => {
                item.classList.toggle('selected', !allSelected);
                if (!allSelected) selectedItems.push(item);
            });

            updateSelectedIndexes();
            updateSelectionCount();
            this.textContent = allSelected ? '全选' : '取消全选';
        });

        document.querySelector('#img-dl-modal .download-btn').addEventListener('click', downloadSelected);
        document.getElementById('img-dl-overlay').addEventListener('click', closeModal);

        imageUrls = new Set(getPageImages());
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();