// ==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 `![${img.filename}](${img.url}${title})`; }).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 图片语法,支持多种格式: // ![alt](url "caption") 或 ![alt](url) 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='';} }); } })();