// ==UserScript== // @name X/Twitter 原图下载 // @namespace http://tampermonkey.net/ // @version 2.1 // @description 在 X(Twitter)推文中添加“下载图片”按钮,一键获取高质量推文显示分辨率原图,按显示名-日期(-序号)命名 // @author ChatGPT // @match *://*.twitter.com/* // @match *://*.x.com/* // @grant none // @run-at document-end // @license MIT // @downloadURL none // ==/UserScript== (async function () { 'use strict'; function formatDate(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}${m}${d}`; } async function downloadImage(url, filename) { try { const res = await fetch(url, { mode: 'cors' }); if (!res.ok) throw new Error('Network response was not ok'); const blob = await res.blob(); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(blobUrl); } catch (err) { console.error('Download failed', err); } } function addDownloadButton(tweet) { if (tweet.querySelector('.download-images-btn')) return; const actions = tweet.querySelector('div[role="group"]'); if (!actions) return; const btn = document.createElement('button'); btn.innerText = '下载图片'; btn.className = 'download-images-btn'; btn.style.cssText = 'margin-left:8px;cursor:pointer;background:#1da1f2;color:#fff;border:none;padding:4px 8px;border-radius:4px;font-size:12px;'; btn.type = 'button'; btn.addEventListener('click', async (e) => { e.stopPropagation(); e.preventDefault(); // 获取显示名:从 data-testid="User-Name" 中所有 dir="ltr" 的 div 取第一个 textContent let name = 'unknown'; const container = tweet.querySelector('[data-testid="User-Name"]'); if (container) { const divs = Array.from(container.querySelectorAll('div[dir="ltr"]')); for (const div of divs) { const text = div.textContent.trim(); if (text) { name = text; break; } } } name = name.replace(/[\\/:*?"<>|]/g, '_'); // 日期 const timeEl = tweet.querySelector('time'); const date = timeEl ? formatDate(new Date(timeEl.dateTime)) : 'unknown'; // 图片 URLs const imgs = tweet.querySelectorAll('img[src*="pbs.twimg.com/media"]'); const urls = Array.from(imgs, img => img.src.split('?')[0] + '?name=large&format=png'); if (urls.length === 0) return; const single = urls.length === 1; for (let i = 0; i < urls.length; i++) { const url = urls[i]; const idx = i + 1; const filename = single ? `${name}-${date}.png` : `${name}-${date}-${String(idx).padStart(2, '0')}.png`; await downloadImage(url, filename); } }); actions.appendChild(btn); } const observer = new MutationObserver(muts => { muts.forEach(m => m.addedNodes.forEach(n => { if (n.nodeType === 1) { if (n.matches('article[role="article"]')) addDownloadButton(n); else n.querySelectorAll('article[role="article"]').forEach(addDownloadButton); } })); }); observer.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll('article[role="article"]').forEach(addDownloadButton); })();