Greasy Fork

Greasy Fork is available in English.

X/Twitter 原图下载

在 X(Twitter)推文中添加“下载图片”按钮,一键获取高质量推文显示分辨率原图,按显示名-日期(-序号)命名

当前为 2025-05-25 提交的版本,查看 最新版本

// ==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
// ==/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);
})();