您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 X 推文中添加“下载图片”按钮,下载 PNG 原图,可通过菜单选择命名规则
// ==UserScript== // @name X/Twitter 推特原图下载 // @namespace http://tampermonkey.net/ // @version 2.2 // @description 在 X 推文中添加“下载图片”按钮,下载 PNG 原图,可通过菜单选择命名规则 // @author ChatGPT // @match *://*.twitter.com/* // @match *://*.x.com/* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @license MIT // @run-at document-end // ==/UserScript== (function() { 'use strict'; const STYLE = { DATE_ONLY:1, DATE_HMS:2 }; let namingStyle = GM_getValue('namingStyle', STYLE.DATE_ONLY); let menuIDs = []; function registerMenu() { // unregister old for (const id of menuIDs) { try { GM_unregisterMenuCommand(id); } catch(e) {} } menuIDs = []; // register date only const label1 = (namingStyle === STYLE.DATE_ONLY ? '✅ ' : '❌ ') + '命名:年月日'; menuIDs.push(GM_registerMenuCommand(label1, () => { namingStyle = STYLE.DATE_ONLY; GM_setValue('namingStyle', namingStyle); console.log('命名规则切换为:年月日'); registerMenu(); })); // register date-hms const label2 = (namingStyle === STYLE.DATE_HMS ? '✅ ' : '❌ ') + '命名:年月日-时分秒'; menuIDs.push(GM_registerMenuCommand(label2, () => { namingStyle = STYLE.DATE_HMS; GM_setValue('namingStyle', namingStyle); console.log('命名规则切换为:年月日-时分秒'); registerMenu(); })); } registerMenu(); function formatDate(d) { const Y = d.getFullYear(), M = String(d.getMonth()+1).padStart(2,'0'), D = String(d.getDate()).padStart(2,'0'); let s = `${Y}${M}${D}`; if (namingStyle === STYLE.DATE_HMS) { const h = String(d.getHours()).padStart(2,'0'); const m = String(d.getMinutes()).padStart(2,'0'); const sec = String(d.getSeconds()).padStart(2,'0'); s += `-${h}${m}${sec}`; } return s; } async function download(url, filename) { const res = await fetch(url); if (!res.ok) return; 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); } function addButton(tweet) { if (tweet.querySelector('.download-images-btn')) return; const grp = tweet.querySelector('div[role="group"]'); if (!grp) 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.addEventListener('click', async e => { e.preventDefault(); e.stopPropagation(); let name = 'unknown'; const c = tweet.querySelector('[data-testid="User-Name"]'); if (c) for (const div of c.querySelectorAll('div[dir="ltr"]')) { const t = div.textContent.trim(); if (t) { name = t; break; } } name = name.replace(/[\\/:*?"<>|]/g, '_'); const timeEl = tweet.querySelector('time'); const dt = timeEl ? new Date(timeEl.dateTime) : new Date(); const ds = formatDate(dt); 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) return; const single = urls.length === 1; for (let i = 0; i < urls.length; i++) { const fn = single ? `${name}-${ds}.png` : `${name}-${ds}-${String(i+1).padStart(2,'0')}.png`; await download(urls[i], fn); } }); grp.appendChild(btn); } const observer = new MutationObserver(muts => muts.forEach(m => m.addedNodes.forEach(n => { if (n.nodeType === 1) { if (n.matches('article[role="article"]')) addButton(n); else n.querySelectorAll('article[role="article"]').forEach(addButton); } }))); observer.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll('article[role="article"]').forEach(addButton); })();