// ==UserScript==
// @name SVG嗅探器
// @namespace http://tampermonkey.net/
// @version 1.5
// @description 下载网页中的SVG图片
// @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/149/149071.png
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
buttonSize: 30,
activeColor: '#3498db',
hoverColor: '#2980b9',
zIndex: 99999,
positionOffset: 25,
touchDelay: 300
};
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, #3498db, #2c3e50);
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: 800px;
max-height: 80vh;
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, #3498db, #2980b9);
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: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.2s;
}
.item-download-btn:hover {
background: #2980b9;
}
.modal-content {
padding: 20px;
overflow-y: auto;
max-height: 60vh;
}
.svg-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
transition: background 0.2s;
justify-content: space-between;
}
.svg-item:hover {
background-color: #f8fafc;
}
.svg-checkbox {
margin-right: 20px;
width: 20px;
height: 20px;
cursor: pointer;
}
.svg-preview {
width: 50px;
height: 50px;
margin-right: 20px;
border: 1px solid #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.svg-preview svg {
max-width: 100%;
max-height: 100%;
}
.svg-name {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.05rem;
color: #2c3e50;
margin-right: 15px;
}
.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(52, 152, 219, 0.6); }
70% { box-shadow: 0 0 0 12px rgba(52, 152, 219, 0); }
100% { box-shadow: 0 0 0 0 rgba(52, 152, 219, 0); }
}
`);
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 globalSvgItems = [];
let svgItemCache = new Map();
let isDragging = false;
let startX, startY, startLeft, startTop;
let dragStartTime = 0;
let touchTimer = null;
let blobUrls = [];
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) {
showSVGList();
}
});
}
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) {
showSVGList();
}
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 inlineExternalResources(svg) {
const linkElements = svg.querySelectorAll('link[rel="stylesheet"]');
linkElements.forEach(link => {
try {
const href = link.getAttribute('href');
if (href) {
const xhr = new XMLHttpRequest();
xhr.open('GET', href, false);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.textContent = xhr.responseText;
link.parentNode.replaceChild(style, link);
}
};
xhr.send();
}
} catch (e) {
link.parentNode.removeChild(link);
}
});
const imageElements = svg.querySelectorAll('image');
imageElements.forEach(img => {
try {
const href = img.getAttribute('href') || img.getAttribute('xlink:href');
if (href && !href.startsWith('data:')) {
const imgObj = new Image();
imgObj.crossOrigin = 'anonymous';
imgObj.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = imgObj.width;
canvas.height = imgObj.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgObj, 0, 0);
const dataUrl = canvas.toDataURL('image/png');
img.setAttribute('href', dataUrl);
img.removeAttribute('xlink:href');
};
imgObj.src = href;
}
} catch (e) {
console.warn('处理SVG图片失败:', e);
}
});
}
function collectSVGs() {
const svgItems = [];
const svgElements = document.querySelectorAll('svg');
svgElements.forEach((svg, index) => {
let name = `SVG ${index + 1}`;
let parent = svg.parentElement;
while (parent) {
if (parent.querySelector('h1, h2, h3, h4, h5, h6, [class*="title"], [class*="name"]')) {
const nameElement = parent.querySelector('h1, h2, h3, h4, h5, h6, [class*="title"], [class*="name"]');
if (nameElement) {
name = nameElement.textContent.trim() || name;
break;
}
}
parent = parent.parentElement;
}
const clonedSvg = svg.cloneNode(true);
inlineExternalResources(clonedSvg);
clonedSvg.removeAttribute('onclick');
clonedSvg.removeAttribute('onmouseover');
clonedSvg.removeAttribute('onmouseout');
const svgId = `svg-${index}-${Date.now()}`;
svgItems.push({
name: name,
svg: clonedSvg.outerHTML,
id: svgId
});
});
return svgItems;
}
function showSVGList() {
const modal = document.getElementById('svgSnifferModal');
const svgList = document.getElementById('svgList');
svgList.innerHTML = '正在扫描页面SVG资源...
';
modal.style.display = 'block';
overlay.style.display = 'block';
setTimeout(() => {
try {
const svgItems = collectSVGs();
globalSvgItems = svgItems;
svgItemCache.clear();
svgItems.forEach(item => {
svgItemCache.set(item.id, item);
});
if (svgItems.length === 0) {
svgList.innerHTML = '没有找到SVG资源
';
return;
}
svgList.innerHTML = '';
svgItems.forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = 'svg-item';
itemElement.innerHTML = `
`;
svgList.appendChild(itemElement);
const itemDownloadBtn = itemElement.querySelector('.item-download-btn');
itemDownloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
const svgId = e.currentTarget.dataset.svgId;
const svgItem = svgItemCache.get(svgId);
if (svgItem) {
console.log('触发单个SVG下载:', svgItem.name, svgItem.id);
downloadSingleSVG(svgItem, true);
} else {
showNotification('未找到该SVG资源,请刷新重试', 'error');
console.error('单个下载失败:SVG项未找到,ID=', svgId);
}
});
});
} catch (error) {
console.error('SVG扫描错误:', error);
svgList.innerHTML = `错误: ${error.message}
`;
}
}, 300);
}
function downloadSingleSVG(svgItem, isSingleDownload = false) {
console.log('进入单个下载函数:', {
name: svgItem.name,
id: svgItem.id,
isSingle: isSingleDownload,
gmDownloadExists: typeof GM_download !== 'undefined',
fileSaverExists: typeof saveAs !== 'undefined'
});
const cleanName = sanitizeFileName(svgItem.name);
const svgContent = `${svgItem.svg}`;
const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' });
if (isSingleDownload) {
try {
const url = URL.createObjectURL(blob);
blobUrls.push(url);
const link = document.createElement('a');
link.href = url;
link.download = `${cleanName}.svg`;
link.style.display = 'none';
document.body.appendChild(link);
link.dispatchEvent(new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
button: 0
}));
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
blobUrls = blobUrls.filter(u => u !== url);
}, 100);
showNotification(`单独下载成功: ${cleanName}.svg`, 'success');
return;
} catch (nativeError) {
console.warn('原生下载失败,尝试FileSaver:', nativeError);
}
try {
if (typeof saveAs !== 'undefined') {
saveAs(blob, `${cleanName}.svg`);
showNotification(`单独下载成功: ${cleanName}.svg`, 'success');
return;
}
} catch (fsError) {
console.warn('FileSaver下载失败,尝试GM_download:', fsError);
}
try {
if (typeof GM_download !== 'undefined') {
const url = URL.createObjectURL(blob);
blobUrls.push(url);
GM_download({
url: url,
name: `${cleanName}.svg`,
mimetype: 'image/svg+xml',
onload: () => {
showNotification(`单独下载成功: ${cleanName}.svg`, 'success');
URL.revokeObjectURL(url);
blobUrls = blobUrls.filter(u => u !== url);
},
onerror: (gmError) => {
showNotification('单独下载失败,请检查浏览器权限', 'error');
console.error('GM_download单独下载失败:', gmError);
URL.revokeObjectURL(url);
blobUrls = blobUrls.filter(u => u !== url);
}
});
return;
}
} catch (gmFinalError) {
console.error('所有单独下载方案失败:', gmFinalError);
showNotification('单独下载失败,建议复制SVG手动保存', 'error');
}
}
if (typeof GM_download !== 'undefined') {
const url = URL.createObjectURL(blob);
blobUrls.push(url);
GM_download({
url: url,
name: `${cleanName}.svg`,
mimetype: 'image/svg+xml',
onload: () => {
if (!isSingleDownload) showNotification(`下载成功: ${cleanName}.svg`, 'success');
URL.revokeObjectURL(url);
blobUrls = blobUrls.filter(u => u !== url);
},
onerror: (error) => {
console.error('GM_download失败:', error);
if (!isSingleDownload) showNotification('GM_download失败,尝试备用方案', 'warning');
fallbackDownload(blob, `${cleanName}.svg`, isSingleDownload);
}
});
return;
}
try {
saveAs(blob, `${cleanName}.svg`);
if (!isSingleDownload) showNotification(`下载成功: ${cleanName}.svg`, 'success');
} catch (error) {
console.error('下载失败:', error);
fallbackDownload(blob, `${cleanName}.svg`, isSingleDownload);
}
}
function downloadMultipleSVGs(items) {
const zip = new JSZip();
items.forEach(item => {
const cleanName = sanitizeFileName(item.name);
const svgContent = `${item.svg}`;
zip.file(`${cleanName}.svg`, svgContent);
});
zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
}).then(content => {
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:]/g, '-');
const zipName = `svg_collection_${timestamp}.zip`;
try {
saveAs(content, zipName);
showNotification(`批量下载成功: ${zipName}`, 'success');
} catch (error) {
console.error('ZIP下载失败:', error);
fallbackDownload(content, zipName);
}
}).catch(error => {
console.error('ZIP创建失败:', error);
showNotification('创建压缩包失败,建议单个下载', 'error');
items.slice(0, 3).forEach(item => downloadSingleSVG(item, true));
});
}
function downloadSelected() {
const checkboxes = document.querySelectorAll('.svg-checkbox:checked');
if (checkboxes.length === 0) {
showNotification('请至少选择一个SVG!', 'warning');
return;
}
const selectedItems = [];
checkboxes.forEach(checkbox => {
const id = checkbox.dataset.id;
const item = svgItemCache.get(id) || globalSvgItems.find(i => i.id === id);
if (item) {
selectedItems.push(item);
}
});
if (selectedItems.length === 0) {
showNotification('没有找到选中的SVG项目!', 'warning');
return;
}
clearBlobUrls();
if (selectedItems.length === 1) {
downloadSingleSVG(selectedItems[0], false);
} else {
downloadMultipleSVGs(selectedItems);
}
}
function fallbackDownload(blob, fileName, isSingleDownload = false) {
try {
const url = URL.createObjectURL(blob);
blobUrls.push(url);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
button: 0
});
link.dispatchEvent(event);
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
blobUrls = blobUrls.filter(u => u !== url);
}, 100);
const msg = isSingleDownload ?
`单独下载成功(备用): ${fileName}` :
`下载成功(备用): ${fileName}`;
showNotification(msg, 'success');
} catch (error) {
console.error('备用下载失败:', error);
const msg = isSingleDownload ?
'单独下载失败,请检查浏览器下载权限' :
'下载失败,请检查浏览器下载权限';
showNotification(msg, 'error');
clearBlobUrls();
}
}
function sanitizeFileName(name) {
return name
.replace(/[^\\w\\u4e00-\\u9fa5\\-\\s]/g, '_')
.replace(/\\s+/g, '_')
.substring(0, 50)
.trim() || 'unnamed_svg';
}
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 copySelected() {
const checkboxes = document.querySelectorAll('.svg-checkbox:checked');
if (checkboxes.length === 0) {
showNotification('请至少选择一个SVG!', 'warning');
return;
}
let combinedCode = '';
checkboxes.forEach(checkbox => {
const id = checkbox.dataset.id;
const item = svgItemCache.get(id) || globalSvgItems.find(i => i.id === id);
if (item) {
combinedCode += `${item.svg}\n\n`;
}
});
try {
GM_setClipboard(combinedCode, 'text');
showNotification(`已复制 ${checkboxes.length} 个SVG代码`, 'success');
} catch (e) {
const textArea = document.createElement('textarea');
textArea.value = combinedCode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification(`已复制 ${checkboxes.length} 个SVG代码 (备用方法)`, 'success');
}
}
function setupEventListeners() {
document.querySelector('.close-btn').addEventListener('click', () => {
document.getElementById('svgSnifferModal').style.display = 'none';
overlay.style.display = 'none';
clearBlobUrls();
svgItemCache.clear();
});
overlay.addEventListener('click', () => {
document.getElementById('svgSnifferModal').style.display = 'none';
overlay.style.display = 'none';
clearBlobUrls();
svgItemCache.clear();
});
document.getElementById('batchDownloadBtn').addEventListener('click', downloadSelected);
document.querySelector('.copy-btn').addEventListener('click', copySelected);
document.getElementById('selectAll').addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.svg-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = e.target.checked;
});
});
window.addEventListener('beforeunload', () => {
clearBlobUrls();
svgItemCache.clear();
});
}
function clearBlobUrls() {
blobUrls.forEach(url => {
try {
URL.revokeObjectURL(url);
} catch (e) {
console.warn('清理Blob URL失败:', e);
}
});
blobUrls = [];
}
function init() {
initRadarButton();
setupEventListeners();
setTimeout(() => {
radarButton.style.display = 'flex';
}, 100);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();