// ==UserScript==
// @name Image Stash Panel v1.4 ZIP & Single
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 图片采集、暂存、下载(单张/ZIP+进度条)、Markdown智能导入导出、拖拽、CC抓取+剪贴板
// @author StreamL+Copilot
// @match *://*/*
// @grant GM_download
// @grant GM_setClipboard
// ==/UserScript==
(function() {
'use strict';
// 插入 JSZip
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
document.head.appendChild(script);
script.onload = () => { initPanel(); };
function initPanel() {
const DOUBLE_KEY = 'c';
const DOUBLE_KEY_DELAY = 400;
const SAVE_KEY = 'd';
const SAVE_KEY_DELAY = 400;
const images = []; // { url, filename, caption }
let lastHoverEl = null;
let lastKeyTime = 0;
let lastSaveKeyTime = 0;
/* ========== 创建面板 ========== */
const panel = document.createElement('div');
panel.id = 'image-stash-panel';
panel.style.position = 'fixed';
panel.style.top = '100px';
panel.style.right = '20px'; // 改为右侧
panel.style.width = '340px'; // 稍微加宽
panel.style.maxHeight = '600px'; // 增加最大高度
panel.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; // 渐变背景
panel.style.border = 'none';
panel.style.borderRadius = '12px'; // 更圆润的边角
panel.style.boxShadow = '0 8px 24px rgba(0,0,0,0.3)';
panel.style.zIndex = '9999';
panel.style.cursor = 'move';
panel.style.padding = '12px';
panel.style.overflow = 'hidden';
panel.style.backdropFilter = 'blur(10px)'; // 毛玻璃效果
document.body.appendChild(panel);
// 标题栏
const titleBar = document.createElement('div');
titleBar.textContent = '📸 图片收集器';
titleBar.style.fontSize = '16px';
titleBar.style.fontWeight = 'bold';
titleBar.style.color = '#fff';
titleBar.style.marginBottom = '10px';
titleBar.style.textAlign = 'center';
titleBar.style.textShadow = '0 2px 4px rgba(0,0,0,0.2)';
panel.appendChild(titleBar);
const btnContainer = document.createElement('div');
btnContainer.style.marginBottom = '10px';
btnContainer.style.display = 'flex';
btnContainer.style.flexWrap = 'wrap';
btnContainer.style.gap = '6px';
panel.appendChild(btnContainer);
const listEl = document.createElement('div');
listEl.style.display = 'flex';
listEl.style.flexDirection = 'column';
listEl.style.gap = '6px';
listEl.style.maxHeight = '420px';
listEl.style.overflowY = 'auto';
listEl.style.minHeight = '80px';
listEl.style.padding = '8px';
listEl.style.background = 'rgba(255, 255, 255, 0.95)';
listEl.style.borderRadius = '8px';
listEl.style.boxShadow = 'inset 0 2px 4px rgba(0,0,0,0.1)';
// 美化滚动条
listEl.style.scrollbarWidth = 'thin';
listEl.style.scrollbarColor = '#667eea rgba(255,255,255,0.3)';
panel.appendChild(listEl);
// 占位提示文字
const placeholder = document.createElement('div');
placeholder.innerHTML = '🖼️ 拖拽图片到此
或双击粘贴图片链接
CC 复制图片链接并加入暂存区
DD 另存为(用小标题命名)';
placeholder.style.color = '#999';
placeholder.style.fontSize = '13px';
placeholder.style.textAlign = 'center';
placeholder.style.padding = '20px 10px';
placeholder.style.lineHeight = '1.6';
listEl.appendChild(placeholder);
// 进度条
const progressEl = document.createElement('div');
progressEl.style.width = '100%';
progressEl.style.height = '6px';
progressEl.style.background = 'rgba(255, 255, 255, 0.3)';
progressEl.style.borderRadius = '3px';
progressEl.style.marginTop = '8px';
progressEl.style.overflow = 'hidden';
const progressBar = document.createElement('div');
progressBar.style.width = '0%';
progressBar.style.height = '100%';
progressBar.style.background = 'linear-gradient(90deg, #4caf50, #8bc34a)';
progressBar.style.transition = 'width 0.3s ease';
progressBar.style.boxShadow = '0 0 10px rgba(76, 175, 80, 0.5)';
progressEl.appendChild(progressBar);
panel.appendChild(progressEl);
/* ========== 按钮 ========== */
function createBtn(text, onclick) {
const btn = document.createElement('button');
btn.textContent = text;
btn.style.flex = '1';
btn.style.fontSize = '12px';
btn.style.padding = '6px 10px';
btn.style.background = 'rgba(255, 255, 255, 0.9)';
btn.style.border = 'none';
btn.style.borderRadius = '6px';
btn.style.cursor = 'pointer';
btn.style.fontWeight = '500';
btn.style.color = '#333';
btn.style.transition = 'all 0.2s ease';
btn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
btn.onmouseover = () => {
btn.style.background = '#fff';
btn.style.transform = 'translateY(-2px)';
btn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
};
btn.onmouseout = () => {
btn.style.background = 'rgba(255, 255, 255, 0.9)';
btn.style.transform = 'translateY(0)';
btn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
};
btn.onclick = onclick;
btnContainer.appendChild(btn);
return btn;
}
createBtn('下载单张', () => {
images.forEach(img => {
const name = img.caption ? `${img.caption}_${img.filename}` : img.filename;
GM_download({ url: img.url, name, saveAs: true });
});
});
createBtn('下载全部 ZIP', async () => {
if (images.length === 0) return alert('暂无图片!');
const zip = new JSZip();
for (let i = 0; i < images.length; i++) {
const img = images[i];
try {
const resp = await fetch(img.url);
const blob = await resp.blob();
const name = img.caption ? `${img.caption}_${img.filename}` : img.filename;
zip.file(name, blob);
progressBar.style.width = `${Math.round((i + 1) / images.length * 100)}%`;
} catch(e) {
console.warn('下载图片失败:', img.url, e);
}
}
const content = await zip.generateAsync({ type: 'blob' });
const a = document.createElement('a');
a.href = URL.createObjectURL(content);
a.download = 'images.zip';
a.click();
URL.revokeObjectURL(a.href);
progressBar.style.width = '0%'; // 重置进度
});
createBtn('导出 Markdown', () => {
const blob = new Blob([markdownText()], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'images.md';
a.click();
URL.revokeObjectURL(url);
});
createBtn('复制 Markdown', () => {
GM_setClipboard(markdownText());
alert('Markdown 已复制');
});
createBtn('打开全部', () => {
if (images.length === 0) return alert('暂无图片!');
images.forEach((img, index) => {
setTimeout(() => {
window.open(img.url, '_blank');
}, index * 200); // 每张图片间隔200ms,避免浏览器阻止
});
alert(`正在打开 ${images.length} 张图片到新标签页...`);
});
createBtn('清空', () => {
images.length = 0;
listEl.innerHTML = '';
listEl.appendChild(placeholder);
});
createBtn('导入 Markdown', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.md,text/markdown';
input.onchange = async e => {
const file = e.target.files[0];
if (!file) return;
const text = await file.text();
parseMarkdownAndAddImages(text);
};
input.click();
});
/* ========== 工具函数 ========== */
function getFileName(url) { return url.split('/').pop().split('?')[0]; }
function markdownText() {
return images.map(img => {
const title = img.caption ? ` "${img.caption}"` : '';
return ``;
}).join('\n\n') + '\n';
}
function addImage(url) {
if (!url || images.some(i => i.url === url)) return;
const filename = getFileName(url);
images.push({ url, filename, caption: '' });
// 一旦添加图片,隐藏占位提示
placeholder.style.display = 'none';
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.marginBottom = '4px';
item.style.gap = '4px';
item.style.padding = '2px';
item.style.border = '1px solid #eee';
item.style.borderRadius = '4px';
item.style.background = '#fafafa';
item.draggable = true;
item.innerHTML = `
`;
const input = item.querySelector('input');
const delBtn = item.querySelector('button');
input.addEventListener('input', () => {
const imgData = images.find(i => i.url === url);
if (imgData) imgData.caption = input.value.trim();
});
delBtn.onclick = () => {
const index = images.findIndex(i => i.url === url);
if (index >= 0) images.splice(index, 1);
item.remove();
if (images.length === 0) placeholder.style.display = 'block';
};
// 拖拽排序
item.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', url);
item.classList.add('dragging');
});
item.addEventListener('dragend', () => { item.classList.remove('dragging'); updateImagesOrder(); });
item.addEventListener('dragover', e => {
e.preventDefault();
const draggingEl = listEl.querySelector('.dragging');
if (!draggingEl || draggingEl === item) return;
const rect = item.getBoundingClientRect();
const middleY = rect.top + rect.height / 2;
if (e.clientY < middleY) listEl.insertBefore(draggingEl, item);
else listEl.insertBefore(draggingEl, item.nextSibling);
});
listEl.appendChild(item);
}
function updateImagesOrder() {
const newOrder = [];
listEl.querySelectorAll('div').forEach(div => {
const url = div.querySelector('img').src;
const data = images.find(i => i.url === url);
if (data) newOrder.push(data);
});
images.length = 0;
images.push(...newOrder);
}
function parseMarkdownAndAddImages(mdText) {
// 匹配 Markdown 图片语法,支持多种格式:
//  或 
const regex = /!\[([^\]]*?)\]\(\s*(\S+?)(?:\s+"([^"]*?)")?\s*\)/g;
let match;
while ((match = regex.exec(mdText)) !== null) {
const altText = match[1].trim(); // 图片 alt
const url = match[2].trim(); // 图片 URL
let caption = match[3] || altText; // 小标题,优先使用 title,其次用 alt
// 如果没有小标题,尝试从前面的行提取标题(如 **传统节日 — 中秋**)
if (!caption || caption === '') {
const beforeText = mdText.substring(0, match.index);
const lines = beforeText.split('\n');
// 从后往前找最近的非空行
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (line) {
// 提取 **标题** 或普通文本作为 caption
const titleMatch = line.match(/\*\*([^*]+)\*\*/);
if (titleMatch) {
caption = titleMatch[1].trim();
} else if (!line.startsWith('!')) {
caption = line.replace(/^#+\s*/, '').trim(); // 去除 markdown 标题符号
}
break;
}
}
}
const filename = url.split('/').pop().split('?')[0];
// 避免重复导入
if (!images.some(img => img.url === url)) {
images.push({ url, filename, caption: caption || '' });
// 隐藏占位提示
placeholder.style.display = 'none';
// 创建图片项
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.marginBottom = '4px';
item.style.gap = '4px';
item.style.padding = '2px';
item.style.border = '1px solid #eee';
item.style.borderRadius = '4px';
item.style.background = '#fafafa';
item.draggable = true;
item.innerHTML = `
`;
const input = item.querySelector('input');
const delBtn = item.querySelector('button');
input.addEventListener('input', () => {
const imgData = images.find(i => i.url === url);
if (imgData) imgData.caption = input.value.trim();
});
delBtn.onclick = () => {
const index = images.findIndex(i => i.url === url);
if (index >= 0) images.splice(index, 1);
item.remove();
if (images.length === 0) placeholder.style.display = 'block';
};
// 拖拽排序
item.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', url);
item.classList.add('dragging');
});
item.addEventListener('dragend', () => { item.classList.remove('dragging'); updateImagesOrder(); });
item.addEventListener('dragover', e => {
e.preventDefault();
const draggingEl = listEl.querySelector('.dragging');
if (!draggingEl || draggingEl === item) return;
const rect = item.getBoundingClientRect();
const middleY = rect.top + rect.height / 2;
if (e.clientY < middleY) listEl.insertBefore(draggingEl, item);
else listEl.insertBefore(draggingEl, item.nextSibling);
});
listEl.appendChild(item);
}
}
}
/* ========== 鼠标 & 快捷键抓取 ========== */
const NAV_KEYS = ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'];
function findCurrentImage() {
const imgs = Array.from(document.images);
let best = null;
let bestScore = 0;
for (const img of imgs) {
const rect = img.getBoundingClientRect();
if (rect.width < 50 || rect.height < 50 || rect.bottom <= 0 || rect.right <= 0 || rect.top >= window.innerHeight || rect.left >= window.innerWidth) continue;
const area = rect.width * rect.height;
if (area > bestScore) { bestScore = area; best = img; }
}
return best;
}
document.addEventListener('mousemove', e => { lastHoverEl = e.target; });
document.addEventListener('keydown', e => {
if (NAV_KEYS.includes(e.key)) {
setTimeout(() => { const img = findCurrentImage(); if(img) lastHoverEl = img; }, 100);
}
});
document.addEventListener('keydown', e => {
if (e.key.toLowerCase() !== DOUBLE_KEY) return;
const now = Date.now();
if (now - lastKeyTime < DOUBLE_KEY_DELAY) {
let img = lastHoverEl?.closest?.('img') || lastHoverEl;
if (!img || img.tagName !== 'IMG') img = findCurrentImage();
if (img?.src) {
addImage(img.src);
try { GM_setClipboard(img.src); } catch(e){console.warn(e);}
} else console.warn('[ImageStash] 未找到可抓取的图片');
}
lastKeyTime = now;
});
/* ========== DD 快捷键:另存为(小标题_原图名) ========== */
async function saveImageAsJPG(imgSrc, saveName) {
try {
const img = new Image();
img.crossOrigin = 'anonymous';
img.referrerPolicy = 'no-referrer-when-downgrade';
img.src = imgSrc;
await img.decode();
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
canvas.getContext('2d').drawImage(img, 0, 0);
canvas.toBlob(blob => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = saveName;
a.click();
URL.revokeObjectURL(a.href);
console.log('[ImageStash] 已另存为:', saveName, blob.type);
}, 'image/jpeg', 0.95);
} catch(e) {
console.error('[ImageStash] 保存图片失败:', e);
// 降级方案:直接使用 GM_download
GM_download({ url: imgSrc, name: saveName, saveAs: true });
}
}
document.addEventListener('keydown', e => {
if (e.key.toLowerCase() !== SAVE_KEY) return;
const now = Date.now();
if (now - lastSaveKeyTime < SAVE_KEY_DELAY) {
let img = lastHoverEl?.closest?.('img') || lastHoverEl;
if (!img || img.tagName !== 'IMG') img = findCurrentImage();
if (img?.src) {
// 查找暂存区中是否有这张图片
const existingImg = images.find(i => i.url === img.src);
const filename = getFileName(img.src);
// 确保文件名有 .jpg 扩展名
const baseFilename = filename.replace(/\.[^.]+$/, '');
const saveName = existingImg && existingImg.caption
? `${existingImg.caption}_${baseFilename}.jpg`
: `${baseFilename}.jpg`;
// 使用 canvas 方法保存(确保是 jpg 格式)
saveImageAsJPG(img.src, saveName);
} else {
console.warn('[ImageStash] 未找到可保存的图片');
}
}
lastSaveKeyTime = now;
});
/* ========== 双击粘贴剪贴板 ========== */
panel.addEventListener('dblclick', async () => {
try {
const text = await navigator.clipboard.readText();
if (text) addImage(text.trim());
} catch(e) { console.warn('[ImageStash] 无法读取剪贴板', e); }
});
/* ========== 拖拽图片到面板 ========== */
panel.addEventListener('dragover', e => e.preventDefault());
panel.addEventListener('drop', e => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
for (const f of files) addImage(URL.createObjectURL(f));
const urls = e.dataTransfer.getData('text/uri-list');
if (urls) urls.split('\n').forEach(u => u && addImage(u.trim()));
});
/* ========== 拖动面板 ========== */
let isDragging = false, offsetX=0, offsetY=0;
panel.addEventListener('mousedown', e => {
if(e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'IMG') return;
isDragging = true;
offsetX = e.clientX - panel.getBoundingClientRect().left;
offsetY = e.clientY - panel.getBoundingClientRect().top;
panel.style.transition = 'none';
});
document.addEventListener('mousemove', e => {
if (!isDragging) return;
panel.style.left = `${e.clientX - offsetX}px`;
panel.style.top = `${e.clientY - offsetY}px`;
});
document.addEventListener('mouseup', () => { if(isDragging){isDragging=false;panel.style.transition='';} });
}
})();