// ==UserScript==
// @name 网页图片采集器
// @namespace http://tampermonkey.net/
// @version 3.9
// @description 无限制下载网页中的图片
// @author YourName
// @match *://*/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @grant GM_notification
// @grant GM_download
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @icon https://cdn-icons-png.flaticon.com/512/2107/2107957.png
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
buttonSize: 30,
activeColor: '#e74c3c',
hoverColor: '#c0392b',
zIndex: 99999,
positionOffset: 25,
touchDelay: 300,
supportFormats: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'tiff'],
maxPreviewSize: 50,
loadTimeout: 5000,
maxTitleLength: 30
};
GM_addStyle(`
.radar-container {position:fixed;z-index:${CONFIG.zIndex};cursor:move;transition:transform 0.2s;touch-action:none;}
.radar-button {width:${CONFIG.buttonSize}px;height:${CONFIG.buttonSize}px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#e74c3c,#922b21);box-shadow:0 6px 18px rgba(0,0,0,0.3),0 0 0 4px rgba(255,255,255,0.15),inset 0 0 12px rgba(0,0,0,0.3);cursor:pointer;border:none;outline:none;position:relative;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;animation:pulse 2s infinite;transition:transform 0.3s,box-shadow 0.3s;}
.radar-button:hover {transform:scale(1.05);box-shadow:0 8px 22px rgba(0,0,0,0.4),0 0 0 4px rgba(255,255,255,0.25),inset 0 0 15px rgba(0,0,0,0.4);}
.radar-button:active {transform:scale(0.95);}
.radar-icon {width:24px;height:24px;position:relative;display:flex;justify-content:center;align-items:center;filter:drop-shadow(0 0 2px rgba(255,255,255,0.5));animation:radar-scan 4s linear infinite;}
.radar-icon svg {width:100%;height:100%;}
#svgSnifferModal {display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:900px;max-height:85vh;background:white;z-index:10000;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,0.3);overflow:hidden;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;}
.modal-header {background:linear-gradient(135deg,#e74c3c,#922b21);color:white;padding:18px 25px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(255,255,255,0.2);}
.modal-header h2 {margin:0;font-size:1.4rem;font-weight:600;}
.close-btn {background:none;border:none;color:white;font-size:1.8rem;cursor:pointer;width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background 0.2s;}
.close-btn:hover {background:rgba(255,255,255,0.2);}
.action-bar {display:flex;justify-content:space-between;align-items:center;padding:15px 25px;background:#f8f9fa;border-bottom:1px solid #e9ecef;}
.select-all-control {display:flex;align-items:center;gap:10px;font-size:1rem;}
.action-buttons {display:flex;gap:12px;}
.action-btn {padding:10px 20px;border:none;border-radius:6px;cursor:pointer;font-weight:600;font-size:0.95rem;transition:all 0.2s;}
.download-btn {background:#27ae60;color:white;}
.download-btn:hover {background:#219653;transform:translateY(-2px);}
.copy-btn {background:#2980b9;color:white;}
.copy-btn:hover {background:#2573a7;transform:translateY(-2px);}
.item-download-btn {padding:6px 12px;background:#e74c3c;color:white;border:none;border-radius:4px;cursor:pointer;font-size:0.85rem;transition:background 0.2s;white-space:nowrap;}
.item-download-btn:hover {background:#c0392b;}
.modal-content {padding:20px;overflow-y:auto;max-height:65vh;}
.svg-item {
display:flex;
align-items:center;
padding:15px;
border-bottom:1px solid #eee;
transition:background 0.2s;
justify-content:space-between;
flex-wrap:nowrap;
min-width:0;
}
.svg-item:hover {background-color:#f8fafc;}
.svg-checkbox {margin-right:20px;width:20px;height:20px;cursor:pointer;flex-shrink:0;}
.svg-preview {width:${CONFIG.maxPreviewSize}px;height:${CONFIG.maxPreviewSize}px;margin-right:20px;border:1px solid #e0e0e0;display:flex;align-items:center;justify-content:center;border-radius:6px;background:#f9f9f9;box-shadow:0 2px 6px rgba(0,0,0,0.05);overflow:hidden;flex-shrink:0;}
.svg-preview img,.svg-preview svg {max-width:100%;max-height:100%;object-fit:contain;}
.svg-info {
flex-grow:1;
margin-right:15px;
min-width:0;
}
.svg-name {
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
font-size:1.05rem;
color:#2c3e50;
margin-bottom:4px;
max-width:100%;
tooltip-delay: 300ms;
}
.svg-meta {
font-size:0.85rem;
color:#6e6e73;
display:flex;
gap:15px;
flex-wrap:wrap;
max-width:100%;
}
.overlay {position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;display:none;}
.loading {text-align:center;padding:30px;font-size:1.2rem;color:#666;}
.copy-notification {position:fixed;top:30px;left:50%;transform:translateX(-50%);background-color:#27ae60;color:white;padding:12px 25px;border-radius:6px;z-index:100000;opacity:0;transition:opacity 0.5s;pointer-events:none;white-space:nowrap;font-weight:500;box-shadow:0 4px 12px rgba(0,0,0,0.15);}
@keyframes radar-scan {0% {transform:rotate(0deg);} 100% {transform:rotate(360deg);}}
@keyframes pulse {0% {box-shadow:0 0 0 0 rgba(231,76,60,0.6);} 70% {box-shadow:0 0 0 12px rgba(231,76,60,0);} 100% {box-shadow:0 0 0 0 rgba(231,76,60,0);}}
.temp-visible-for-scan {display:block !important;visibility:visible !important;opacity:1 !important;position:absolute !important;top:-9999px !important;left:-9999px !important;width:auto !important;height:auto !important;}
`);
const radarContainer = document.createElement('div');
radarContainer.className = 'radar-container';
radarContainer.id = 'radarContainer';
const radarButton = document.createElement('div');
radarButton.className = 'radar-button';
radarButton.id = 'radarButton';
radarButton.innerHTML = `
`;
radarContainer.appendChild(radarButton);
document.body.appendChild(radarContainer);
const svgModal = document.createElement('div');
svgModal.id = 'svgSnifferModal';
svgModal.innerHTML = `
`;
document.body.appendChild(svgModal);
const overlay = document.createElement('div');
overlay.className = 'overlay';
document.body.appendChild(overlay);
const copyNotification = document.createElement('div');
copyNotification.className = 'copy-notification';
document.body.appendChild(copyNotification);
let globalImageItems = [];
let imageItemCache = new Map();
let isDragging = false;
let startX, startY, startLeft, startTop;
let dragStartTime = 0;
let touchTimer = null;
let blobUrls = [];
let tempVisibleElements = [];
function initRadarButton() {
const domain = location.hostname.replace(/\./g, '-');
const positionKey = `radarPosition_${domain}`;
const savedPosition = GM_getValue(positionKey);
if (savedPosition) {
radarContainer.style.left = `${savedPosition.x}px`;
radarContainer.style.top = `${savedPosition.y}px`;
} else {
radarContainer.style.right = `${CONFIG.positionOffset}px`;
radarContainer.style.bottom = `${CONFIG.positionOffset}px`;
}
radarContainer.addEventListener('mousedown', startDrag);
radarContainer.addEventListener('touchstart', startDrag, { passive: false });
radarButton.addEventListener('click', (e) => {
if (!isDragging && Date.now() - dragStartTime > CONFIG.touchDelay) {
showImageList();
}
});
}
function startDrag(e) {
e.preventDefault();
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
const computedStyle = window.getComputedStyle(radarContainer);
startLeft = parseInt(computedStyle.left) || 0;
startTop = parseInt(computedStyle.top) || 0;
if (computedStyle.right !== 'auto') {
const rightPos = parseInt(computedStyle.right);
startLeft = window.innerWidth - rightPos - CONFIG.buttonSize;
radarContainer.style.right = 'auto';
radarContainer.style.left = `${startLeft}px`;
}
startX = clientX;
startY = clientY;
dragStartTime = Date.now();
if (e.type === 'touchstart') {
touchTimer = setTimeout(() => {
isDragging = true;
radarContainer.style.transition = 'none';
}, CONFIG.touchDelay);
} else {
isDragging = true;
}
document.addEventListener('mousemove', drag);
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
const dx = clientX - startX;
const dy = clientY - startY;
radarContainer.style.left = `${startLeft + dx}px`;
radarContainer.style.top = `${startTop + dy}px`;
radarContainer.style.right = 'auto';
}
function endDrag(e) {
if (touchTimer) {
clearTimeout(touchTimer);
touchTimer = null;
}
if (!isDragging) {
if (Date.now() - dragStartTime < CONFIG.touchDelay) {
showImageList();
}
return;
}
isDragging = false;
radarContainer.style.transition = '';
document.removeEventListener('mousemove', drag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('mouseup', endDrag);
document.removeEventListener('touchend', endDrag);
const domain = location.hostname.replace(/\./g, '-');
const positionKey = `radarPosition_${domain}`;
const rect = radarContainer.getBoundingClientRect();
GM_setValue(positionKey, {
x: rect.left,
y: rect.top
});
}
function tempShowHiddenElements() {
tempVisibleElements = [];
const hiddenSelectors = [
'div[style*="display:none"]',
'div[style*="visibility:hidden"]',
'div[style*="opacity:0"]',
'.errorpage[style*="display:none"]',
'[class*="hidden"]',
'[hidden]'
];
hiddenSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
const originalStyle = {
display: el.style.display,
visibility: el.style.visibility,
opacity: el.style.opacity,
position: el.style.position,
top: el.style.top,
left: el.style.left,
width: el.style.width,
height: el.style.height,
className: el.className
};
tempVisibleElements.push({ el, originalStyle });
el.classList.add('temp-visible-for-scan');
el.style.display = '';
el.style.visibility = '';
el.style.opacity = '';
});
});
}
function restoreHiddenElements() {
tempVisibleElements.forEach(({ el, originalStyle }) => {
el.classList.remove('temp-visible-for-scan');
el.style.display = originalStyle.display;
el.style.visibility = originalStyle.visibility;
el.style.opacity = originalStyle.opacity;
el.style.position = originalStyle.position;
el.style.top = originalStyle.top;
el.style.left = originalStyle.left;
el.style.width = originalStyle.width;
el.style.height = originalStyle.height;
el.className = originalStyle.className;
});
tempVisibleElements = [];
}
async function collectImagesFromCss(cssUrl) {
const imageUrls = [];
try {
const response = await fetch(cssUrl, {
headers: {
'Accept': 'text/css,*/*;q=0.1'
},
credentials: 'same-origin'
});
if (!response.ok) throw new Error(`CSS请求失败: ${response.status}`);
const cssText = await response.text();
const bgUrlRegex = /background-image\s*:\s*url\(["']?([^"']+)["']?\)/gi;
let match;
while ((match = bgUrlRegex.exec(cssText)) !== null) {
if (match[1]) {
const fullUrl = new URL(match[1], cssUrl).href;
const ext = getFileExtension(fullUrl).toLowerCase();
if (CONFIG.supportFormats.includes(ext)) {
imageUrls.push(fullUrl);
}
}
}
} catch (e) {
console.warn('采集CSS中的图片失败:', cssUrl, e);
}
return imageUrls;
}
async function collectAllCssResources() {
const cssUrls = [];
const linkElements = document.querySelectorAll('link[rel="stylesheet"]');
linkElements.forEach(link => {
const href = link.getAttribute('href');
if (href) {
cssUrls.push(new URL(href, window.location.href).href);
}
});
const styleElements = document.querySelectorAll('style');
styleElements.forEach(style => {
const bgUrlRegex = /background-image\s*:\s*url\(["']?([^"']+)["']?\)/gi;
let match;
while ((match = bgUrlRegex.exec(style.textContent)) !== null) {
if (match[1]) {
const fullUrl = new URL(match[1], window.location.href).href;
const ext = getFileExtension(fullUrl).toLowerCase();
if (CONFIG.supportFormats.includes(ext)) {
cssUrls.push(`inline:${fullUrl}`);
}
}
}
});
const allImageUrls = [];
for (const cssUrl of cssUrls) {
if (cssUrl.startsWith('inline:')) {
allImageUrls.push(cssUrl.replace('inline:', ''));
} else {
const imagesFromCss = await collectImagesFromCss(cssUrl);
allImageUrls.push(...imagesFromCss);
}
}
return allImageUrls;
}
function truncateTitle(title) {
if (!title || title.length <= CONFIG.maxTitleLength) {
return title;
}
return title.substring(0, CONFIG.maxTitleLength) + '...';
}
async function collectImages() {
const imageItems = [];
const processedUrls = new Set();
let cssImageUrls = [];
try {
cssImageUrls = await collectAllCssResources();
} catch (e) {
console.warn('CSS图片采集异常:', e);
}
tempShowHiddenElements();
try {
const imgElements = document.querySelectorAll('img');
for (const img of imgElements) {
try {
let imgUrl = img.src || img.dataset.src || img.dataset.original;
if (!imgUrl || processedUrls.has(imgUrl)) continue;
const fullUrl = new URL(imgUrl, window.location.href).href;
const ext = getFileExtension(fullUrl).toLowerCase();
if (!CONFIG.supportFormats.includes(ext)) continue;
const imgInfo = await new Promise((resolve) => {
const timer = setTimeout(() => {
resolve({
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
url: fullUrl,
name: getImageName(fullUrl, img.alt),
format: ext,
width: img.width || '未知',
height: img.height || '未知',
type: 'img-tag',
preview: fullUrl
});
}, CONFIG.loadTimeout);
if (img.complete) {
clearTimeout(timer);
resolve({
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
url: fullUrl,
name: getImageName(fullUrl, img.alt),
format: ext,
width: img.naturalWidth || img.width || '未知',
height: img.naturalHeight || img.height || '未知',
type: 'img-tag',
preview: fullUrl
});
} else {
img.onload = () => {
clearTimeout(timer);
resolve({
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
url: fullUrl,
name: getImageName(fullUrl, img.alt),
format: ext,
width: img.naturalWidth || img.width || '未知',
height: img.naturalHeight || img.height || '未知',
type: 'img-tag',
preview: fullUrl
});
};
img.onerror = () => {
clearTimeout(timer);
resolve(null);
};
}
});
if (imgInfo) {
processedUrls.add(imgUrl);
imageItems.push(imgInfo);
}
} catch (e) {
console.warn('采集
标签失败:', e);
}
}
const elementsWithBg = document.querySelectorAll('*');
for (const el of elementsWithBg) {
try {
const bgStyle = window.getComputedStyle(el).backgroundImage;
if (!bgStyle || bgStyle === 'none' || processedUrls.has(bgStyle)) continue;
const bgUrls = bgStyle.match(/url\(["']?([^"']+)["']?\)/g);
if (!bgUrls) continue;
for (const bgUrl of bgUrls) {
try {
const match = bgUrl.match(/url\(["']?([^"']+)["']?\)/);
if (!match || !match[1]) continue;
let imgUrl = match[1];
if (processedUrls.has(imgUrl)) continue;
const fullUrl = new URL(imgUrl, window.location.href).href;
const ext = getFileExtension(fullUrl).toLowerCase();
if (!CONFIG.supportFormats.includes(ext)) continue;
const imgInfo = {
id: `bg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
url: fullUrl,
name: `背景图-${el.tagName.toLowerCase()}-${Date.now().toString().slice(-4)}`,
format: ext,
width: '背景图',
height: '背景图',
type: 'background',
preview: fullUrl
};
processedUrls.add(imgUrl);
imageItems.push(imgInfo);
} catch (e) {
console.warn('采集背景图失败:', e);
}
}
} catch (e) {
console.warn('处理背景图样式失败:', e);
}
}
for (const imgUrl of cssImageUrls) {
try {
if (!imgUrl || processedUrls.has(imgUrl)) continue;
const fullUrl = new URL(imgUrl, window.location.href).href;
const ext = getFileExtension(fullUrl).toLowerCase();
if (!CONFIG.supportFormats.includes(ext)) continue;
const imgInfo = {
id: `css-${Date.now()}-${Math.random().toString(36).slice(2)}`,
url: fullUrl,
name: `CSS图片-${Date.now().toString().slice(-4)}`,
format: ext,
width: 'CSS引用',
height: 'CSS引用',
type: 'css-image',
preview: fullUrl
};
processedUrls.add(imgUrl);
imageItems.push(imgInfo);
} catch (e) {
console.warn('采集CSS图片失败:', e);
}
}
const svgElements = document.querySelectorAll('svg');
for (const svg of svgElements) {
try {
const svgId = `svg-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const svgContent = svg.outerHTML;
const svgUrl = URL.createObjectURL(new Blob([svgContent], { type: 'image/svg+xml' }));
blobUrls.push(svgUrl);
const imgInfo = {
id: svgId,
url: svgUrl,
name: `SVG图片-${Date.now().toString().slice(-4)}`,
format: 'svg',
width: svg.naturalWidth || svg.width.baseVal.value || '自适应',
height: svg.naturalHeight || svg.height.baseVal.value || '自适应',
type: 'svg-tag',
preview: svgUrl,
svgContent: svgContent
};
imageItems.push(imgInfo);
} catch (e) {
console.warn('采集SVG标签失败:', e);
}
}
} catch (e) {
console.error('图片采集主流程异常:', e);
} finally {
restoreHiddenElements();
}
return imageItems;
}
function getFileExtension(url) {
const path = new URL(url).pathname;
const lastPart = path.split('/').pop();
const extMatch = lastPart.match(/\.([^.]+)$/);
return extMatch ? extMatch[1] : '';
}
function getImageName(url, altText) {
if (altText && altText.trim()) {
return altText.trim();
}
const path = new URL(url).pathname;
const fileName = path.split('/').pop().split('?')[0].split('#')[0];
return fileName || `未知图片-${Date.now().toString().slice(-6)}`;
}
async function showImageList() {
const modal = document.getElementById('svgSnifferModal');
const svgList = document.getElementById('svgList');
const imageCountEl = document.getElementById('imageCount');
svgList.innerHTML = '正在扫描页面图片资源(含CSS/隐藏元素)...
';
modal.style.display = 'block';
overlay.style.display = 'block';
try {
const imageItems = await collectImages();
globalImageItems = imageItems;
imageItemCache.clear();
imageCountEl.textContent = imageItems.length;
imageItems.forEach(item => {
imageItemCache.set(item.id, item);
});
if (imageItems.length === 0) {
svgList.innerHTML = '没有找到任何图片资源(已尝试采集隐藏元素和CSS)
';
return;
}
svgList.innerHTML = '';
imageItems.forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = 'svg-item';
const truncatedTitle = truncateTitle(item.name);
let previewHtml = '';
if (item.format === 'svg' && item.svgContent) {
previewHtml = ``;
} else {
previewHtml = `
`;
}
itemElement.innerHTML = `
${previewHtml}
${truncatedTitle}
格式: ${item.format}
类型: ${item.type === 'img-tag' ? '图片标签' : item.type === 'background' ? '背景图' : item.type === 'css-image' ? 'CSS图片' : 'SVG标签'}
`;
svgList.appendChild(itemElement);
const itemDownloadBtn = itemElement.querySelector('.item-download-btn');
itemDownloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
const imgId = e.currentTarget.dataset.imgId;
const imgItem = imageItemCache.get(imgId);
if (imgItem) {
downloadSingleImage(imgItem);
} else {
showNotification('未找到该图片资源,请刷新重试', 'error');
}
});
});
} catch (error) {
console.error('图片扫描错误:', error);
svgList.innerHTML = `扫描错误: ${error.message}(已尝试恢复隐藏元素)
`;
}
}
function downloadSingleImage(imgItem) {
try {
const cleanName = sanitizeFileName(`${imgItem.name}.${imgItem.format}`);
if (imgItem.format === 'svg' && imgItem.svgContent) {
const svgContent = `${imgItem.svgContent}`;
const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' });
saveAs(blob, cleanName);
showNotification(`单独下载成功: ${cleanName}`, 'success');
return;
}
if (typeof GM_download !== 'undefined') {
GM_download({
url: imgItem.url,
name: cleanName,
mimetype: `image/${imgItem.format}`,
onload: () => showNotification(`单独下载成功: ${cleanName}`, 'success'),
onerror: (error) => {
console.warn('GM_download失败,尝试备用方案:', error);
fallbackImageDownload(imgItem.url, cleanName);
}
});
return;
}
fallbackImageDownload(imgItem.url, cleanName);
} catch (error) {
console.error('单独下载失败:', error);
showNotification('单独下载失败,请检查浏览器权限', 'error');
}
}
function fallbackImageDownload(imgUrl, fileName) {
try {
const link = document.createElement('a');
link.href = imgUrl;
link.download = fileName;
link.style.display = 'none';
if (imgUrl.includes('http') && !imgUrl.includes(window.location.hostname)) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
const blobUrl = URL.createObjectURL(blob);
blobUrls.push(blobUrl);
link.href = blobUrl;
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl);
blobUrls = blobUrls.filter(u => u !== blobUrl);
}, 100);
}, `image/${fileName.split('.').pop()}`);
};
img.src = imgUrl;
} else {
document.body.appendChild(link);
link.click();
setTimeout(() => document.body.removeChild(link), 100);
}
showNotification(`单独下载成功(备用): ${fileName}`, 'success');
} catch (error) {
console.error('备用下载失败:', error);
showNotification('下载失败,建议直接复制图片链接', 'error');
}
}
async function downloadSelectedImages() {
const checkboxes = document.querySelectorAll('.svg-checkbox:checked');
if (checkboxes.length === 0) {
showNotification('请至少选择一个图片!', 'warning');
return;
}
const selectedItems = [];
checkboxes.forEach(checkbox => {
const id = checkbox.dataset.id;
const item = imageItemCache.get(id) || globalImageItems.find(i => i.id === id);
if (item) {
selectedItems.push(item);
}
});
if (selectedItems.length === 0) {
showNotification('没有找到选中的图片项目!', 'warning');
return;
}
if (selectedItems.length === 1) {
downloadSingleImage(selectedItems[0]);
return;
}
const zip = new JSZip();
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:]/g, '-');
const zipName = `网页图片合集_${timestamp}.zip`;
let completedCount = 0;
const svgList = document.getElementById('svgList');
svgList.innerHTML = `正在打包图片(${completedCount}/${selectedItems.length})...
`;
for (const [index, item] of selectedItems.entries()) {
try {
const cleanName = sanitizeFileName(`${index + 1}_${item.name}.${item.format}`);
if (item.format === 'svg' && item.svgContent) {
zip.file(cleanName, `${item.svgContent}`);
completedCount++;
updateZipProgress(completedCount, selectedItems.length);
continue;
}
const response = await fetch(item.url, {
credentials: 'same-origin',
timeout: CONFIG.loadTimeout
});
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
zip.file(cleanName, arrayBuffer, { binary: true });
} catch (err) {
console.warn(`添加图片失败: ${item.name}`, err);
zip.file(`加载失败_${cleanName}.txt`, `图片加载失败: ${item.url}\n错误原因: ${err.message}`);
}
completedCount++;
updateZipProgress(completedCount, selectedItems.length);
}
try {
const content = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
});
saveAs(content, zipName);
showNotification(`批量下载成功: ${zipName}(共${selectedItems.length}张)`, 'success');
showImageList();
} catch (error) {
console.error('ZIP创建失败:', error);
showNotification('创建压缩包失败,建议分批下载', 'error');
showImageList();
}
}
function updateZipProgress(completed, total) {
const svgList = document.getElementById('svgList');
svgList.innerHTML = `正在打包图片(${completed}/${total})...
`;
}
function copySelectedImageUrls() {
const checkboxes = document.querySelectorAll('.svg-checkbox:checked');
if (checkboxes.length === 0) {
showNotification('请至少选择一个图片!', 'warning');
return;
}
let urls = [];
checkboxes.forEach(checkbox => {
const id = checkbox.dataset.id;
const item = imageItemCache.get(id) || globalImageItems.find(i => i.id === id);
if (item) {
urls.push(`${item.name}: ${item.url}`);
}
});
const urlText = urls.join('\n\n');
try {
GM_setClipboard(urlText, 'text');
showNotification(`已复制 ${checkboxes.length} 个图片链接`, 'success');
} catch (e) {
const textArea = document.createElement('textarea');
textArea.value = urlText;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification(`已复制 ${checkboxes.length} 个图片链接 (备用方法)`, 'success');
}
}
function setupEventListeners() {
document.querySelector('.close-btn').addEventListener('click', () => {
document.getElementById('svgSnifferModal').style.display = 'none';
overlay.style.display = 'none';
clearBlobUrls();
imageItemCache.clear();
restoreHiddenElements();
});
overlay.addEventListener('click', () => {
document.getElementById('svgSnifferModal').style.display = 'none';
overlay.style.display = 'none';
clearBlobUrls();
imageItemCache.clear();
restoreHiddenElements();
});
document.getElementById('batchDownloadBtn').addEventListener('click', downloadSelectedImages);
document.querySelector('.copy-btn').addEventListener('click', copySelectedImageUrls);
document.getElementById('selectAll').addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.svg-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = e.target.checked;
});
});
window.addEventListener('beforeunload', () => {
clearBlobUrls();
imageItemCache.clear();
restoreHiddenElements();
});
}
function clearBlobUrls() {
blobUrls.forEach(url => {
try {
URL.revokeObjectURL(url);
} catch (e) {
console.warn('清理Blob URL失败:', e);
}
});
blobUrls = [];
}
function sanitizeFileName(name) {
return name
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, '_')
.substring(0, 80)
.trim() || 'unnamed_image';
}
function showNotification(message, type = 'info') {
const colors = {
info: '#3498db',
success: '#27ae60',
warning: '#f39c12',
error: '#e74c3c'
};
copyNotification.textContent = message;
copyNotification.style.backgroundColor = colors[type] || colors.info;
copyNotification.style.opacity = '1';
setTimeout(() => {
copyNotification.style.opacity = '0';
}, 3000);
}
function init() {
initRadarButton();
setupEventListeners();
setTimeout(() => {
radarButton.style.display = 'flex';
}, 100);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();