Greasy Fork

来自缓存

Greasy Fork is available in English.

图片爬虫|图片批量自动打包下载|网页图片批量下载器V6

自动爬取网页图片并支持预览下载,多线程并发下载,无限滚动加载

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         图片爬虫|图片批量自动打包下载|网页图片批量下载器V6
// @namespace    http://tampermonkey.net/
// @version      6.0
// @description  自动爬取网页图片并支持预览下载,多线程并发下载,无限滚动加载
// @author       白虎万岁
// @license      MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_openInTab
// @grant        GM_notification
// @grant        GM_log
// @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';

    // ─── 配置 ────────────────────────────────────────────────────────────────
    const CONFIG = {
        CONCURRENT_DOWNLOADS: 6,
        RETRY_MAX: 3,
        RETRY_DELAY_BASE: 800,
        TIMEOUT: 30000,
        LAZY_BATCH: 20,
        LAZY_DELAY: 16,
        MAX_PREVIEW_SIZE: 2000,
        ITEMS_PER_LOAD: GM_getValue('itemsPerLoad', 20),
    };

    // ─── 全局状态 ─────────────────────────────────────────────────────────────
    let imageUrls = new Set();
    let status, modal, overlay, downloadBtn, previewModal;
    let progressBar, progressText;
    let imgObserver; // 懒加载观察器
    let lazyCheckScheduled = false; // 滚动节流标志

    // 无限加载相关
    let displayedCount = 0;
    let allImages = [];
    let isLoading = false;
    let touchStartY = 0;
    let touchEndY = 0;


    // ─── 并发控制器 ──────────────────────────────────────────────────────────
    async function runConcurrent(tasks, concurrency, onProgress) {
        const results = new Array(tasks.length);
        let index = 0;
        let done = 0;

        async function worker() {
            while (index < tasks.length) {
                const i = index++;
                try {
                    results[i] = { ok: true, value: await tasks[i]() };
                } catch (e) {
                    results[i] = { ok: false, error: e };
                }
                done++;
                if (onProgress) onProgress(done, tasks.length);
            }
        }

        const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, worker);
        await Promise.all(workers);
        return results;
    }

    // ─── DOM 创建 ─────────────────────────────────────────────────────────────
    function createElements() {
        // 状态提示
        status = document.createElement('div');
        status.style.cssText = `
            position:fixed;bottom:80px;right:20px;z-index:2147483647;
            padding:10px 16px;background:rgba(0,0,0,0.75);color:#fff;
            border-radius:6px;font-size:14px;display:none;
            max-width:320px;line-height:1.5;
        `;

        progressBar = document.createElement('div');
        progressBar.style.cssText = `
            height:4px;background:#4CAF50;border-radius:2px;
            width:0%;transition:width 0.2s;margin-top:6px;display:none;
        `;
        progressText = document.createElement('span');
        status.appendChild(progressText);
        status.appendChild(progressBar);

        // 模态框
        modal = document.createElement('div');
        modal.className = 'image-downloader-modal';
        modal.innerHTML = `
            <div class="modal-header">
                <span class="modal-title">图片预览 (V6)</span>
                <div class="header-controls">
                    <label class="items-per-load-control">
                        每次加载:
                        <input type="number" class="items-per-load-input" min="5" max="100" value="20">
                        张
                    </label>
                    <button class="modal-btn refresh-btn">刷新</button>
                </div>
                <span class="modal-close">×</span>
            </div>
            <div class="pull-to-refresh">
                <span class="refresh-icon">⬇️ 下拉加载更多</span>
            </div>
            <div class="modal-content"></div>
            <div class="loading-indicator" style="display:none;text-align:center;padding:20px;">
                <span>加载中...</span>
            </div>
            <div class="modal-footer">
                <div class="footer-left">
                    <button class="modal-btn select-all-btn">全选</button>
                    <span class="selected-count">已选择: 0</span>
                </div>
                <div class="footer-right">
                    <button class="modal-btn download-btn" disabled>单张下载</button>
                    <button class="modal-btn download-zip-btn" disabled>打包下载</button>
                </div>
            </div>
        `;

        // 遮罩
        overlay = document.createElement('div');
        overlay.className = 'modal-overlay';

        // 大图预览模态框
        previewModal = document.createElement('div');
        previewModal.className = 'image-preview-modal';
        previewModal.innerHTML = `
            <button class="preview-close">×</button>
            <img class="preview-image" src="" alt="预览">
            <div class="preview-info">
                <a class="preview-link" href="" target="_blank" rel="noopener">在新标签页打开</a>
            </div>
            <div class="preview-actions">
                <button class="modal-btn preview-download-btn">下载此图</button>
                <button class="modal-btn preview-close-btn">关闭</button>
            </div>
        `;

        // 悬浮按钮
        downloadBtn = document.createElement('div');
        downloadBtn.className = 'image-downloader-btn';
        downloadBtn.innerHTML = '📷';
        downloadBtn.title = '图片批量下载';
    }

    // ─── 样式 ─────────────────────────────────────────────────────────────────
    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .image-downloader-btn {
                position:fixed;top:50%;right:0;z-index:2147483647 !important;
                width:50px;height:50px;border-radius:8px 0 0 8px;
                background:linear-gradient(145deg,#FF9800,#F57C00) !important;
                color:#fff !important;cursor:pointer;display:flex !important;
                align-items:center;justify-content:center;
                box-shadow:0 4px 12px rgba(255,152,0,0.4) !important;
                font-size:24px;user-select:none;transition:all 0.3s;
                transform:translateY(-50%);
                border:none !important;
                padding:0 !important;
                margin:0 !important;
                opacity:1 !important;
                visibility:visible !important;
            }
            .image-downloader-btn:hover {
                background:linear-gradient(145deg,#FFA726,#FB8C00) !important;
                box-shadow:0 6px 16px rgba(255,152,0,0.6) !important;
                transform:translateY(-50%) translateX(-5px);
            }

            .image-downloader-modal {
                position:fixed;top:50%;left:50%;
                transform:translate(-50%,-50%);
                z-index:2147483646;width:70vw;height:70vh;
                background:#fff;border-radius:16px;
                box-shadow:0 8px 32px rgba(0,0,0,0.15);
                display:none;flex-direction:column;overflow:hidden;
            }
            .modal-header {
                padding:16px 24px;background:#fff;
                border-bottom:1px solid #eee;
                display:flex;justify-content:space-between;align-items:center;
                gap:16px;flex-wrap:wrap;
            }
            .modal-title { font-size:18px;font-weight:600;color:#1976D2; }
            .header-controls {
                display:flex;align-items:center;gap:12px;
            }
            .items-per-load-control {
                display:flex;align-items:center;gap:6px;
                color:#666;font-size:13px;
                background:#f5f5f5;padding:6px 12px;border-radius:6px;
            }
            .items-per-load-input {
                width:50px;padding:4px 6px;border:1px solid #ddd;
                border-radius:4px;font-size:13px;text-align:center;
            }
            .items-per-load-input:focus {
                outline:none;border-color:#2196F3;box-shadow:0 0 4px rgba(33,150,243,0.3);
            }
            .modal-close {
                cursor:pointer;font-size:24px;color:#666;transition:all 0.2s;
                width:32px;height:32px;display:flex;align-items:center;
                justify-content:center;border-radius:50%;background:#f5f5f5;
            }
            .modal-close:hover { color:#fff;background:#f44336; }

            .pull-to-refresh {
                padding:12px;text-align:center;color:#999;font-size:12px;
                background:#f9f9f9;border-bottom:1px solid #eee;
                transition:all 0.3s;
            }
            .pull-to-refresh.pulling {
                background:#e3f2fd;color:#1976D2;
            }

            .modal-content {
                flex:1;padding:20px;overflow-y:auto;overflow-x:hidden;
                display:grid;
                grid-template-columns:repeat(auto-fill,minmax(160px,1fr));
                gap:16px;background:#f5f5f5;
                touch-action:pan-y;
            }
            .modal-content::-webkit-scrollbar { width:8px; }
            .modal-content::-webkit-scrollbar-thumb { background:#ccc;border-radius:4px; }

            .image-item {
                position:relative;padding-top:100%;
                border:2px solid transparent;border-radius:12px;
                cursor:pointer;transition:all 0.2s;
                background:#fff;overflow:hidden;
            }
            .image-item::before {
                content:'';position:absolute;top:10px;right:10px;
                width:20px;height:20px;border-radius:50%;
                border:2px solid #fff;background:rgba(0,0,0,0.3);
                z-index:2;
            }
            .image-item.selected::before { background:#2196F3; }
            .image-item:hover { transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,0.15); }
            .image-item.selected { border-color:#2196F3; }
            .image-item img {
                position:absolute;top:0;left:0;width:100%;height:100%;
                object-fit:cover;opacity:0;transition:opacity 0.3s;
            }
            .image-item img.loaded { opacity:1; }

            /* 预览按钮 */
            .preview-btn {
                position:absolute;top:10px;left:10px;
                width:24px;height:24px;border-radius:50%;
                background:rgba(0,0,0,0.5);color:#fff;border:none;
                cursor:pointer;z-index:2;opacity:0;
                display:flex;align-items:center;justify-content:center;
                font-size:14px;transition:all 0.2s;
            }
            .image-item:hover .preview-btn { opacity:1; }
            .preview-btn:hover { background:rgba(33,150,243,0.8); }

            .loading-indicator {
                color:#999;font-size:14px;padding:20px;
            }

            .modal-footer {
                padding:16px 24px;border-top:1px solid #eee;
                display:flex;justify-content:space-between;align-items:center;
                background:#fff;flex-wrap:wrap;gap:10px;
            }
            .footer-left, .footer-right {
                display:flex;align-items:center;gap:10px;
            }
            .modal-btn {
                padding:8px 16px;border:none;border-radius:6px;cursor:pointer;
                background:#2196F3;color:#fff;font-size:14px;
                transition:all 0.2s;
            }
            .modal-btn:hover { background:#1976D2; }
            .modal-btn:disabled { background:#ccc;cursor:not-allowed; }
            .path-btn { background:#4CAF50; }
            .path-btn:hover { background:#388E3C; }
            .selected-count {
                color:#666;font-size:14px;background:#f5f5f5;
                padding:6px 12px;border-radius:6px;
            }
            .modal-overlay {
                position:fixed;inset:0;
                background:rgba(0,0,0,0.5);
                z-index:2147483645;display:none;
            }

            /* 大图预览模态框 */
            .image-preview-modal {
                position:fixed;top:0;left:0;right:0;bottom:0;
                background:rgba(0,0,0,0.9);z-index:2147483647;
                display:none;flex-direction:column;align-items:center;justify-content:center;
            }
            .image-preview-modal.active { display:flex; }
            .preview-image {
                max-width:90vw;max-height:80vh;object-fit:contain;
                border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5);
            }
            .preview-info {
                color:#fff;margin-top:16px;font-size:14px;text-align:center;
            }
            .preview-info a {
                color:#64b5f6;text-decoration:none;word-break:break-all;
            }
            .preview-info a:hover { text-decoration:underline; }
            .preview-actions {
                position:absolute;bottom:20px;display:flex;gap:12px;
            }
            .preview-actions .modal-btn { min-width:100px; }
            .preview-close {
                position:absolute;top:20px;right:20px;
                width:40px;height:40px;border-radius:50%;
                background:rgba(255,255,255,0.2);color:#fff;border:none;
                cursor:pointer;font-size:24px;display:flex;
                align-items:center;justify-content:center;
            }
            .preview-close:hover { background:rgba(255,255,255,0.3); }

            @media (max-width: 768px) {
                .image-downloader-modal {
                    width:90vw;height:90vh;
                }
            }
        `;
        document.head.appendChild(style);
    }

    // ─── 图片采集 ─────────────────────────────────────────────────────────────
    function collectPageImages() {
        const images = new Set();

        document.querySelectorAll('img').forEach(img => {
            if (!isValidImage(img)) return;
            const candidates = [
                img.getAttribute('ess-data'),
                img.dataset.src,
                img.dataset.original,
                img.getAttribute('data-original'),
                img.getAttribute('data-src'),
                img.getAttribute('data-actualsrc'),
                img.getAttribute('data-echo'),
                img.getAttribute('data-lazy'),
                img.getAttribute('data-url'),
                img.getAttribute('data-original-src'),
            ];
            candidates.forEach(src => {
                if (src && isValidImageUrl(src)) images.add(normalizeUrl(src));
            });
        });

        document.querySelectorAll('*').forEach(el => {
            try {
                const bg = window.getComputedStyle(el).backgroundImage;
                if (!bg || bg === 'none') return;
                const matches = bg.match(/url\(['"]?(.*?)['"]?\)/g) || [];
                matches.forEach(u => {
                    const clean = u.replace(/url\(['"]?(.*?)['"]?\)/, '$1');
                    if (isValidImageUrl(clean)) images.add(normalizeUrl(clean));
                });
            } catch (_) {}
        });

        document.querySelectorAll('picture source').forEach(src => {
            (src.srcset || '').split(',').forEach(s => {
                const url = s.trim().split(' ')[0];
                if (isValidImageUrl(url)) images.add(normalizeUrl(url));
            });
        });

        return Array.from(images).slice(0, CONFIG.MAX_PREVIEW_SIZE);
    }

    function normalizeUrl(url) {
        if (!url) return url;
        if (url.startsWith('//')) return location.protocol + url;
        if (url.startsWith('/')) return location.origin + url;
        if (url.startsWith('./') || url.startsWith('../')) {
            try { return new URL(url, location.href).href; } catch (_) {}
        }
        return url;
    }

    function isValidImage(img) {
        if (!img) return false;
        if (img.complete) return img.naturalWidth > 0 || img.naturalHeight > 0;
        const r = img.getBoundingClientRect();
        return r.width > 0 || r.height > 0;
    }

    function isValidImageUrl(url) {
        if (!url || typeof url !== 'string') return false;
        try {
            const clean = url.split('?')[0].split('#')[0].toLowerCase();
            const ext = (clean.match(/\.([^.]+)$/) || [])[1];
            return ['jpg','jpeg','png','gif','webp','bmp','svg','ico','avif'].includes(ext);
        } catch (_) { return false; }
    }

    function getImageExtension(url) {
        const m = url.split('?')[0].split('#')[0].match(/\.([^.]+)$/);
        return m ? m[1].toLowerCase() : 'jpg';
    }

    // ─── 无限加载逻辑 ─────────────────────────────────────────────────────────
    function loadMoreImages() {
        if (isLoading || displayedCount >= allImages.length) return;

        isLoading = true;
        const loadingIndicator = modal.querySelector('.loading-indicator');
        loadingIndicator.style.display = 'block';

        setTimeout(() => {
            const content = modal.querySelector('.modal-content');
            const end = Math.min(displayedCount + CONFIG.ITEMS_PER_LOAD, allImages.length);
            const frag = document.createDocumentFragment();

            for (let i = displayedCount; i < end; i++) {
                const url = allImages[i];
                const item = document.createElement('div');
                item.className = 'image-item';

                const previewBtn = document.createElement('button');
                previewBtn.className = 'preview-btn';
                previewBtn.innerHTML = '🔍';
                previewBtn.title = '预览大图';

                const img = document.createElement('img');
                img.dataset.src = url;
                img.alt = '';

                item.appendChild(previewBtn);
                item.appendChild(img);
                item.addEventListener('click', (e) => {
                    if (!e.target.closest('.preview-btn')) {
                        item.classList.toggle('selected');
                        updateSelectedCount();
                    }
                });
                frag.appendChild(item);
            }

            content.appendChild(frag);
            displayedCount = end;

            requestAnimationFrame(() => lazyLoadVisible(content));
            loadingIndicator.style.display = 'none';
            isLoading = false;

            // 更新提示文字
            const refreshIcon = modal.querySelector('.refresh-icon');
            if (displayedCount >= allImages.length) {
                refreshIcon.textContent = '✅ 已加载全部 ' + allImages.length + ' 张图片';
            } else {
                refreshIcon.textContent = `⬇️ 已加载 ${displayedCount}/${allImages.length},继续下拉加载`;
            }
        }, 300);
    }

    // ─── 预览 ────────────────────────────────────────────────────────────────
    function showPreview() {
        const content = modal.querySelector('.modal-content');
        content.innerHTML = '';
        displayedCount = 0;

        modal.style.display = 'flex';
        overlay.style.display = 'block';
        updateSelectedCount();

        // 初始加载第一批
        loadMoreImages();

        // 滚动节流
        content.onscroll = () => {
            if (!lazyCheckScheduled) {
                lazyCheckScheduled = true;
                requestAnimationFrame(() => {
                    lazyLoadVisible(content);
                    lazyCheckScheduled = false;
                });
            }

            // 检测滚到底部,自动加载更多
            const isAtBottom = content.scrollHeight - content.scrollTop - content.clientHeight < 100;
            if (isAtBottom && !isLoading && displayedCount < allImages.length) {
                loadMoreImages();
            }
        };

        // 手机下拉加载
        let pullStartY = 0;
        const pullToRefresh = modal.querySelector('.pull-to-refresh');

        content.addEventListener('touchstart', (e) => {
            pullStartY = e.changedTouches[0].clientY;
            touchStartY = e.changedTouches[0].clientY;
        }, false);

        content.addEventListener('touchmove', (e) => {
            const currentY = e.changedTouches[0].clientY;
            const diff = currentY - pullStartY;

            if (content.scrollTop === 0 && diff > 0) {
                pullToRefresh.classList.add('pulling');
                pullToRefresh.textContent = diff > 80 ? '⬆️ 释放加载更多' : '⬇️ 下拉加载更多';
            }
        }, false);

        content.addEventListener('touchend', (e) => {
            touchEndY = e.changedTouches[0].clientY;
            const diff = touchEndY - touchStartY;

            pullToRefresh.classList.remove('pulling');

            // 下拉超过80px触发加载
            if (content.scrollTop === 0 && diff > 80 && !isLoading && displayedCount < allImages.length) {
                loadMoreImages();
            }
        }, false);
    }

    // 记录已观察的图片,避免重复
    const observedImgs = new WeakSet();

    function lazyLoadVisible(container) {
        if (!imgObserver) return;
        container.querySelectorAll('img[data-src]:not([src])').forEach(img => {
            if (!observedImgs.has(img)) {
                observedImgs.add(img);
                imgObserver.observe(img);
            }
        });
    }

    function updateSelectedCount() {
        const n = modal.querySelectorAll('.image-item.selected').length;
        modal.querySelector('.selected-count').textContent = `已选择: ${n}`;
        modal.querySelector('.download-btn').disabled = n === 0;
        modal.querySelector('.download-zip-btn').disabled = n === 0;
    }

    // ─── 进度显示 ─────────────────────────────────────────────────────────────
    function showProgress(msg, pct) {
        progressText.textContent = msg;
        if (pct !== undefined) {
            progressBar.style.display = 'block';
            progressBar.style.width = pct + '%';
        } else {
            progressBar.style.display = 'none';
        }
        status.style.display = 'block';
    }

    function hideStatus() {
        status.style.display = 'none';
        progressBar.style.display = 'none';
        progressBar.style.width = '0%';
    }

    function showStatus(msg, duration = 2000) {
        progressText.textContent = msg;
        progressBar.style.display = 'none';
        status.style.display = 'block';
        setTimeout(hideStatus, duration);
    }

    // ─── 下载功能 ─────────────────────────────────────────────────────────────
    function downloadImage(url, attempt = 0) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                responseType: 'blob',
                headers: { 'Referer': location.href, 'User-Agent': navigator.userAgent },
                timeout: CONFIG.TIMEOUT,
                onload(res) {
                    if (res.status === 200) return resolve(res.response);
                    if (attempt < CONFIG.RETRY_MAX) {
                        setTimeout(() => downloadImage(url, attempt + 1).then(resolve, reject),
                            CONFIG.RETRY_DELAY_BASE * (attempt + 1));
                    } else {
                        reject(new Error(`HTTP ${res.status}`));
                    }
                },
                onerror(e) {
                    if (attempt < CONFIG.RETRY_MAX) {
                        setTimeout(() => downloadImage(url, attempt + 1).then(resolve, reject),
                            CONFIG.RETRY_DELAY_BASE * (attempt + 1));
                    } else {
                        reject(new Error('网络错误'));
                    }
                },
                ontimeout() {
                    if (attempt < CONFIG.RETRY_MAX) {
                        setTimeout(() => downloadImage(url, attempt + 1).then(resolve, reject),
                            CONFIG.RETRY_DELAY_BASE * (attempt + 1));
                    } else {
                        reject(new Error('超时'));
                    }
                }
            });
        });
    }

    async function downloadIndividual(images) {
        showProgress(`准备下载 ${images.length} 张图片...`, 0);

        let failed = 0;
        const tasks = images.map(img => async () => {
            const blob = await downloadImage(img.src);
            const name = img.src.split('/').pop().split('?')[0].split('#')[0];
            const base = name.replace(/\.[^.]+$/, '') || `image_${img.index}`;
            const ext = getImageExtension(img.src);
            const fileName = `${base}.${ext}`;

            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = fileName;
            a.click();
            setTimeout(() => URL.revokeObjectURL(url), 1000);
        });

        await runConcurrent(tasks, CONFIG.CONCURRENT_DOWNLOADS, (done, total) => {
            showProgress(`已下载 ${done}/${total} 张...`, Math.round(done / total * 100));
        });

        closeModal();
        showProgress('下载完成!', 100);
        setTimeout(hideStatus, 3000);
    }

    async function downloadZip(images) {
        showProgress(`准备下载 ${images.length} 张图片...`, 0);

        const zip = new JSZip();
        let failed = 0;

        const tasks = images.map((img, i) => async () => {
            const blob = await downloadImage(img.src);
            const name = img.src.split('/').pop().split('?')[0].split('#')[0];
            const base = name.replace(/\.[^.]+$/, '') || `image_${i}`;
            const ext = getImageExtension(img.src);
            zip.file(`${base}.${ext}`, blob);
        });

        const results = await runConcurrent(tasks, CONFIG.CONCURRENT_DOWNLOADS, (done, total) => {
            showProgress(`正在下载 ${done}/${total} 张...`, Math.round(done / total * 80));
        });

        failed = results.filter(r => !r.ok).length;

        if (results.every(r => !r.ok)) {
            showProgress('所有图片下载失败');
            setTimeout(hideStatus, 3000);
            return;
        }

        showProgress('正在生成压缩包...', 85);

        let zipName;
        try {
            const el = document.getElementsByClassName('f16')[0];
            zipName = el ? el.textContent.trim() : '';
        } catch (_) {}
        if (!zipName) {
            const pageTitle = document.title.replace(/[\\/:*?"<>|]/g, '_');
            const date = new Date().toISOString().split('T')[0];
            zipName = `${pageTitle}_${date}`;
        }

        const content = await zip.generateAsync(
            { type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } },
            meta => showProgress(`打包中 ${Math.round(meta.percent)}%...`, 85 + meta.percent * 0.15)
        );

        const zipFileName = `${zipName}.zip`;

        const url = URL.createObjectURL(content);
        const a = document.createElement('a');
        a.href = url;
        a.download = zipFileName;
        a.click();
        setTimeout(() => URL.revokeObjectURL(url), 1000);

        closeModal();
        showProgress(failed > 0 ? `完成,${failed} 张失败` : '下载完成!', 100);
        setTimeout(hideStatus, 3000);
    }

    function closeModal() {
        modal.style.display = 'none';
        overlay.style.display = 'none';
    }

    // ─── 事件绑定 ─────────────────────────────────────────────────────────────
    function setupEventListeners() {
        downloadBtn.addEventListener('click', () => {
            allImages = Array.from(imageUrls);
            showPreview();
        });

        modal.querySelector('.modal-close').addEventListener('click', closeModal);
        overlay.addEventListener('click', closeModal);

        // 每次加载数量设置
        const itemsPerLoadInput = modal.querySelector('.items-per-load-input');
        itemsPerLoadInput.value = CONFIG.ITEMS_PER_LOAD;
        itemsPerLoadInput.addEventListener('change', (e) => {
            const value = parseInt(e.target.value);
            if (value >= 5 && value <= 100) {
                CONFIG.ITEMS_PER_LOAD = value;
                GM_setValue('itemsPerLoad', value);
            } else {
                e.target.value = CONFIG.ITEMS_PER_LOAD;
            }
        });

        // 刷新
        modal.querySelector('.refresh-btn').addEventListener('click', () => {
            imageUrls = new Set(collectPageImages());
            allImages = Array.from(imageUrls);
            displayedCount = 0;
            showPreview();
        });

        // 全选
        modal.querySelector('.select-all-btn').addEventListener('click', function () {
            const items = modal.querySelectorAll('.image-item');
            const allSelected = Array.from(items).every(i => i.classList.contains('selected'));
            items.forEach(i => i.classList.toggle('selected', !allSelected));
            this.textContent = allSelected ? '全选' : '取消全选';
            updateSelectedCount();
        });

        // 单张下载
        modal.querySelector('.download-btn').addEventListener('click', async () => {
            const selected = Array.from(modal.querySelectorAll('.image-item.selected img'))
                .map((img, i) => ({ src: img.dataset.src || img.src, index: i }));
            if (!selected.length) return;
            await downloadIndividual(selected);
        });

        // 打包下载
        modal.querySelector('.download-zip-btn').addEventListener('click', async () => {
            const selected = Array.from(modal.querySelectorAll('.image-item.selected img'))
                .map((img, i) => ({ src: img.dataset.src || img.src, index: i }));
            if (!selected.length) return;
            await downloadZip(selected);
        });

        // 大图预览功能
        const previewImage = previewModal.querySelector('.preview-image');
        const previewLink = previewModal.querySelector('.preview-link');

        modal.querySelector('.modal-content').addEventListener('click', async (e) => {
            const previewBtn = e.target.closest('.preview-btn');
            if (!previewBtn) return;

            e.stopPropagation();
            const item = previewBtn.closest('.image-item');
            const img = item.querySelector('img');
            const src = img.dataset.src || img.src;

            previewImage.src = src;
            previewLink.href = src;
            previewModal.classList.add('active');
        });

        // 关闭预览
        previewModal.querySelector('.preview-close').addEventListener('click', () => {
            previewModal.classList.remove('active');
        });
        previewModal.querySelector('.preview-close-btn').addEventListener('click', () => {
            previewModal.classList.remove('active');
        });
        previewModal.addEventListener('click', (e) => {
            if (e.target === previewModal) {
                previewModal.classList.remove('active');
            }
        });

        // 预览中下载此图
        previewModal.querySelector('.preview-download-btn').addEventListener('click', async () => {
            const src = previewImage.src;
            const name = src.split('/').pop().split('?')[0].split('#')[0] || 'image';
            const ext = getImageExtension(src);
            const fileName = `${name.replace(/\.[^.]+$/, '')}.${ext}`;

            const blob = await downloadImage(src);
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = fileName;
            a.click();
            setTimeout(() => URL.revokeObjectURL(url), 1000);
        });
    }

    // ─── 初始化 ───────────────────────────────────────────────────────────────
    function init() {
        try {
            imgObserver = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (!entry.isIntersecting) return;
                    const img = entry.target;
                    const src = img.dataset.src;
                    if (!src) return;
                    img.src = src;
                    img.onload = () => img.classList.add('loaded');
                    img.onerror = () => {};
                    imgObserver.unobserve(img);
                });
            }, { rootMargin: '200px' });

            createElements();
            addStyles();
            document.body.appendChild(status);
            document.body.appendChild(downloadBtn);
            document.body.appendChild(modal);
            document.body.appendChild(overlay);
            document.body.appendChild(previewModal);
            setupEventListeners();

            // 确保按钮可见
            setTimeout(() => {
                downloadBtn.style.display = 'flex';
                downloadBtn.style.visibility = 'visible';
                downloadBtn.style.opacity = '1';
            }, 100);

            setTimeout(() => {
                imageUrls = new Set(collectPageImages());
                console.log('[图片下载器 V6] 采集到 ' + imageUrls.size + ' 张图片');
            }, 500);

            console.log('[图片下载器 V6] 初始化成功 - 支持无限滚动加载');
        } catch (e) {
            console.error('[图片下载器 V6] 初始化失败:', e);
        }
    }

    if (navigator.userAgent.includes('Edg/')) {
        window.addEventListener('load', () => setTimeout(init, 500));
    } else if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();