您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
鼠标选择图片!下载按钮已内置于面板中,逻辑更清晰。自动扫描、分辨率筛选、ZIP打包功能有问题。
// ==UserScript== // @name 智能图片批量下载器 (v1.0 - 暂不支持打包下载) // @namespace http://tampermonkey.net/ // @version 1.0 // @license AGPL-3.0-or-later // @description 鼠标选择图片!下载按钮已内置于面板中,逻辑更清晰。自动扫描、分辨率筛选、ZIP打包功能有问题。 // @author jhlxlml // @match *://*/* // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_download // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; // --- 配置 & 状态管理 --- const state = { images: new Map(), zip: new JSZip(), uiVisible: false, isBoxSelecting: false, settings: { minWidth: 600, minHeight: 600, autoScan: true, } }; // --- UI 界面 --- const UI = { init: function() { this.loadSettings(); this.createPanel(); this.createButtons(); this.addEventListeners(); this.initBoxSelection(); // 初始化框选功能 this.initAutoScan(); }, createPanel: function() { GM_addStyle(` #sib-panel { position: fixed; bottom: 80px; right: 20px; width: 360px; max-height: 70vh; background: #fff; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.2); z-index: 99999; display: none; flex-direction: column; font-family: sans-serif; } #sib-header { padding: 10px 15px; background: #f7f7f7; border-bottom: 1px solid #ddd; border-top-left-radius: 8px; border-top-right-radius: 8px; } #sib-header h3 { margin: 0; font-size: 16px; color: #333; } #sib-filter { padding: 10px 15px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; font-size: 12px; border-bottom: 1px solid #ddd;} #sib-filter input[type="number"] { width: 50px; padding: 5px; border: 1px solid #ccc; border-radius: 4px; } #sib-filter label { display:flex; align-items:center; gap: 5px; cursor: pointer;} #sib-image-list { overflow-y: auto; padding: 10px; flex-grow: 1; display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 10px; user-select: none; } .sib-image-item { position: relative; border: 2px solid transparent; border-radius: 4px; overflow: hidden; cursor: pointer; aspect-ratio: 1 / 1; background-color: #eee; transition: border-color 0.2s; } .sib-image-item.selected { border-color: #007bff; } .sib-image-item img { width: 100%; height: 100%; object-fit: cover; } .sib-image-info { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.6); color: white; font-size: 11px; padding: 4px; text-align: center; } .sib-image-item .sib-checkbox { position: absolute; top: 5px; right: 5px; width: 18px; height: 18px; accent-color: #007bff; } #sib-footer { padding: 10px 15px; border-top: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; background: #f7f7f7;} #sib-footer-left { display: flex; align-items: center; gap: 15px; } #sib-footer-right { display: flex; align-items: center; gap: 10px; } #sib-select-all { font-size: 14px; cursor: pointer; display:flex; align-items:center; gap: 5px; } #sib-status { font-size: 12px; color: #555; } .sib-action-btn { background-color: #f0f0f0; color: #888; border: 1px solid #ccc; padding: 6px 12px; border-radius: 5px; font-size: 12px; cursor: not-allowed; transition: all 0.2s;} .sib-action-btn:not(:disabled) { background-color: #ffc107; color: #212529; border-color: #ffc107; cursor: pointer; } .sib-action-btn:not(:disabled):hover { background-color: #e0a800; } .sib-action-btn#sib-download-zip-btn:not(:disabled) { background-color: #17a2b8; color: white; border-color: #17a2b8;} .sib-action-btn#sib-download-zip-btn:not(:disabled):hover { background-color: #117a8b; } #sib-selection-box { position: absolute; background-color: rgba(0, 123, 255, 0.3); border: 1px solid rgba(0, 123, 255, 0.8); z-index: 100000; pointer-events: none; } .sib-main-btn { position: fixed; right: 20px; min-width: 160px; height: 40px; color: white; border: none; border-radius: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.25); cursor: pointer; font-size: 14px; z-index: 99998; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; padding: 0 20px;} #sib-toggle-panel-btn { bottom: 20px; background-color: #007bff; } #sib-toggle-panel-btn:hover { background-color: #0056b3; } #sib-scan-btn { bottom: 20px; right: 200px; background-color: #28a745; display: none;} #sib-scan-btn:hover { background-color: #1e7e34; } `); const panelHTML = ` <div id="sib-panel"> <div id="sib-header"><h3>智能图片批量下载器</h3></div> <div id="sib-filter"> <label>最小尺寸: <input type="number" id="sib-min-width"> x <input type="number" id="sib-min-height"></label> <label title="开启后,每次打开新页面都会自动扫描图片"><input type="checkbox" id="sib-autoscan-checkbox"> 自动扫描</label> </div> <div id="sib-image-list"></div> <div id="sib-footer"> <div id="sib-footer-left"> <label id="sib-select-all"><input type="checkbox" id="sib-select-all-checkbox"> 全选</label> <span id="sib-status">等待扫描...</span> </div> <div id="sib-footer-right"> <button id="sib-download-selected-btn" class="sib-action-btn" disabled>下载选中</button> <button id="sib-download-zip-btn" class="sib-action-btn" disabled>打包下载</button> </div> </div> </div>`; document.body.insertAdjacentHTML('beforeend', panelHTML); document.getElementById('sib-min-width').value = state.settings.minWidth; document.getElementById('sib-min-height').value = state.settings.minHeight; document.getElementById('sib-autoscan-checkbox').checked = state.settings.autoScan; }, createButtons: function() { const toggleBtn = `<button id="sib-toggle-panel-btn" class="sib-main-btn">打开下载面板</button>`; const scanBtn = `<button id="sib-scan-btn" class="sib-main-btn">手动扫描</button>`; document.body.insertAdjacentHTML('beforeend', toggleBtn + scanBtn); }, addEventListeners: function() { document.getElementById('sib-toggle-panel-btn').addEventListener('click', this.togglePanel); document.getElementById('sib-scan-btn').addEventListener('click', () => ImageScanner.scan()); document.getElementById('sib-download-selected-btn').addEventListener('click', Downloader.downloadSelected); document.getElementById('sib-download-zip-btn').addEventListener('click', Downloader.downloadZip); document.getElementById('sib-select-all-checkbox').addEventListener('change', this.toggleSelectAll); document.getElementById('sib-autoscan-checkbox').addEventListener('change', this.handleAutoScanToggle); ['sib-min-width', 'sib-min-height'].forEach(id => { document.getElementById(id).addEventListener('change', this.handleFilterChange); }); }, initBoxSelection: function() { const imageList = document.getElementById('sib-image-list'); let startX, startY, selectionBox; imageList.addEventListener('mousedown', e => { if (e.target !== imageList) return; // 只在容器空白处点击时触发 e.preventDefault(); state.isBoxSelecting = true; startX = e.clientX; startY = e.clientY; selectionBox = document.createElement('div'); selectionBox.id = 'sib-selection-box'; selectionBox.style.left = `${e.pageX}px`; selectionBox.style.top = `${e.pageY}px`; document.body.appendChild(selectionBox); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { if (!state.isBoxSelecting) return; e.preventDefault(); const currentX = e.clientX; const currentY = e.clientY; const boxLeft = Math.min(startX, currentX); const boxTop = Math.min(startY, currentY); const boxWidth = Math.abs(startX - currentX); const boxHeight = Math.abs(startY - currentY); // 注意:style的top/left是相对于viewport的,但getBoundingClientRect也是。 // 如果imageList有滚动,需要换算。为简化,这里假定滚动条在window上。 selectionBox.style.left = `${boxLeft}px`; selectionBox.style.top = `${boxTop}px`; selectionBox.style.width = `${boxWidth}px`; selectionBox.style.height = `${boxHeight}px`; const selectionRect = selectionBox.getBoundingClientRect(); document.querySelectorAll('.sib-image-item').forEach(item => { const itemRect = item.getBoundingClientRect(); const isIntersecting = !(selectionRect.right < itemRect.left || selectionRect.left > itemRect.right || selectionRect.bottom < itemRect.top || selectionRect.top > itemRect.bottom); const checkbox = item.querySelector('.sib-checkbox'); if (checkbox.checked !== isIntersecting) { checkbox.checked = isIntersecting; item.classList.toggle('selected', isIntersecting); } }); } function onMouseUp() { if (state.isBoxSelecting) { state.isBoxSelecting = false; document.body.removeChild(selectionBox); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); UI.updateSummaryStatus(); } } }, initAutoScan: function() { /* 与v1.2相同 */ if (state.settings.autoScan) window.addEventListener('load', () => setTimeout(() => ImageScanner.scan(true), 1000)); }, handleAutoScanToggle: function(e) { state.settings.autoScan = e.target.checked; UI.saveSettings(); }, togglePanel: function() { const panel = document.getElementById('sib-panel'); const scanBtn = document.getElementById('sib-scan-btn'); state.uiVisible = !state.uiVisible; panel.style.display = state.uiVisible ? 'flex' : 'none'; scanBtn.style.display = state.uiVisible ? 'flex' : 'none'; UI.updateToggleButtonText(); }, addImageToList: function(url, width, height) { const list = document.getElementById('sib-image-list'); const item = document.createElement('div'); item.className = 'sib-image-item'; item.dataset.url = url; item.innerHTML = ` <img src="${url}" loading="lazy" onerror="this.parentElement.remove()"> <div class="sib-image-info">${width}x${height}</div> <input type="checkbox" class="sib-checkbox">`; item.querySelector('img').onload = () => item.style.backgroundColor = 'transparent'; const checkbox = item.querySelector('.sib-checkbox'); // Click on item toggles checkbox item.addEventListener('click', e => { if (e.target !== checkbox) { checkbox.checked = !checkbox.checked; item.classList.toggle('selected', checkbox.checked); this.updateSummaryStatus(); } }); // Change on checkbox toggles class checkbox.addEventListener('change', () => { item.classList.toggle('selected', checkbox.checked); this.updateSummaryStatus(); }); list.appendChild(item); }, filterAndDisplayImages: function(isAutoScan = false) { const list = document.getElementById('sib-image-list'); list.innerHTML = ''; let displayedCount = 0; for (const [url, data] of state.images.entries()) { const { width, height } = data; if (width >= state.settings.minWidth && height >= state.settings.minHeight) { this.addImageToList(url, width, height); displayedCount++; } } this.updateSummaryStatus(); if (isAutoScan && displayedCount > 0) this.updateToggleButtonText(displayedCount); }, updateToggleButtonText: function(count) { const btn = document.getElementById('sib-toggle-panel-btn'); if (state.uiVisible) { btn.textContent = '关闭下载面板'; btn.style.backgroundColor = '#007bff'; } else { const foundCount = count || document.querySelectorAll('.sib-image-item').length; if (foundCount > 0) { btn.textContent = `发现 ${foundCount} 张图片`; btn.style.backgroundColor = '#28a745'; } else { btn.textContent = '打开下载面板'; btn.style.backgroundColor = '#007bff'; } } }, updateSummaryStatus: function() { const displayed = document.querySelectorAll('.sib-image-item').length; const total = state.images.size; const selected = document.querySelectorAll('.sib-image-item.selected').length; const statusEl = document.getElementById('sib-status'); statusEl.textContent = `选中: ${selected} / 显示: ${displayed}`; const dlSelectedBtn = document.getElementById('sib-download-selected-btn'); const dlZipBtn = document.getElementById('sib-download-zip-btn'); if (selected > 0) { dlSelectedBtn.disabled = false; dlZipBtn.disabled = false; } else { dlSelectedBtn.disabled = true; dlZipBtn.disabled = true; } document.getElementById('sib-select-all-checkbox').checked = selected > 0 && selected === displayed; }, toggleSelectAll: function(e) { const c = e.target.checked; document.querySelectorAll('.sib-image-item .sib-checkbox').forEach(cb => { cb.checked = c; cb.closest('.sib-image-item').classList.toggle('selected', c); }); UI.updateSummaryStatus(); }, handleFilterChange: function() { state.settings.minWidth = parseInt(document.getElementById('sib-min-width').value) || 0; state.settings.minHeight = parseInt(document.getElementById('sib-min-height').value) || 0; UI.saveSettings(); UI.filterAndDisplayImages(); }, saveSettings: function() { GM_setValue('sib_settings', JSON.stringify(state.settings)); }, loadSettings: function() { const s = GM_getValue('sib_settings'); if (s) { const parsed = JSON.parse(s); delete parsed.maxWidth; delete parsed.maxHeight; Object.assign(state.settings, parsed); } }, }; // --- 图片扫描 (与v1.2相同) --- const ImageScanner = { scan: function(isAutoScan = false) { UI.updateSummaryStatus(); document.getElementById('sib-status').textContent = '扫描中...'; state.images.clear(); document.getElementById('sib-image-list').innerHTML = ''; document.getElementById('sib-select-all-checkbox').checked = false; this.findImagesInTags(); this.findImagesInStyles(); setTimeout(() => { UI.filterAndDisplayImages(isAutoScan); }, 2500); }, findImagesInTags: function() { document.querySelectorAll('img, a, source, div, li, picture').forEach(el => { if (el.tagName === 'A' && el.href && /\.(jpe?g|png|gif|webp|bmp|svg)$/i.test(el.href)) { this.processUrl(el.href); } if (el.tagName === 'IMG' || el.tagName === 'SOURCE') { const p = [el.src, el.dataset.src, el.dataset.original, el.dataset.lazySrc, el.dataset.lazyload, el.getAttribute('data-src-retina')]; if (el.srcset) { p.push(...el.srcset.split(',').map(s => s.trim().split(' ')[0]).filter(Boolean)); } p.forEach(src => this.processUrl(src)); } const g = ['data-src', 'data-original', 'data-bg', 'data-background-image', 'data-lazy-bg']; g.forEach(attr => { if (el.hasAttribute(attr)) this.processUrl(el.getAttribute(attr)); }); }); }, findImagesInStyles: function() { document.querySelectorAll('*').forEach(el => { const b = window.getComputedStyle(el).backgroundImage; if (b && b !== 'none') { const m = b.match(/url\(['"]?(.*?)['"]?\)/); if (m && m[1]) this.processUrl(m[1]); } }); }, processUrl: function(url) { if (!url || typeof url !== 'string' || url.startsWith('data:') || url.length < 10) return; try { const absUrl = new URL(url, window.location.href).href; if (state.images.has(absUrl)) return; state.images.set(absUrl, {width: 0, height: 0}); const img = new Image(); img.onload = () => { state.images.set(absUrl, { width: img.naturalWidth, height: img.naturalHeight }); }; img.onerror = () => { state.images.delete(absUrl); }; img.src = absUrl; } catch (e) {} } }; // --- 下载逻辑 (与v1.2相同) --- const Downloader = { downloadSelected: function() { if (this.disabled) return; const s = document.querySelectorAll('.sib-image-item.selected'); document.getElementById('sib-status').textContent = `开始下载 ${s.length} 张...`; s.forEach((item, index) => { const url = item.dataset.url, filename = Downloader.getFilename(url, index); setTimeout(() => GM_download({ url: url, name: filename, onerror: err => console.error(`下载失败: ${filename}`, err) }), index * 200); }); setTimeout(() => UI.updateSummaryStatus(), s.length * 200 + 500); }, downloadZip: async function() { if (this.disabled) return; const s = document.querySelectorAll('.sib-image-item.selected'); state.zip = new JSZip(); let p = 0; document.getElementById('sib-status').textContent = `打包中: 0/${s.length}`; for (let i = 0; i < s.length; i++) { const item = s[i], url = item.dataset.url, filename = this.getFilename(url, i); try { const r = await this.fetchImage(url); state.zip.file(filename, r, { binary: true }); } catch (e) { state.zip.file(`${filename}_下载失败.txt`, `无法下载:\n${url}\n错误: ${e.message}`); } p++; document.getElementById('sib-status').textContent = `打包中: ${p}/${s.length}`; } document.getElementById('sib-status').textContent = '生成ZIP...'; state.zip.generateAsync({ type: 'blob' }, m => { document.getElementById('sib-status').textContent = `压缩中: ${m.percent.toFixed(0)}%`; }).then(c => { const n = `图片打包_${document.title.replace(/[\\/:*?"<>|]/g, '_') || 'archive'}.zip`; GM_download(URL.createObjectURL(c), n); UI.updateSummaryStatus(); }); }, fetchImage: function(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', onload: r => r.status >= 200 && r.status < 300 ? resolve(r.response) : reject(new Error(`HTTP ${r.status}`)), onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')) }); }); }, getFilename: function(url, index) { try { let n = new URL(url).pathname.split('/').pop(); if (!n || n.indexOf('.') === -1) n = `image.jpg`; return `${index}_${n.replace(/[\\/:*?"<>|]/g, '_').slice(-100)}`; } catch (e) { return `${index}_image.jpg`; } } }; // --- 启动脚本 --- UI.init(); })();