在 Twitter 貼文圖片上加一個浮動下載按鈕,下載 PNG + large 圖片
// ==UserScript==
// @name Twitter Image Inline Download Button
// @namespace Amano_Tools
// @version 1.0
// @description 在 Twitter 貼文圖片上加一個浮動下載按鈕,下載 PNG + large 圖片
// @author Amano
// @match https://twitter.com/*
// @match https://x.com/*
// @match https://pbs.twimg.com/media/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const BUTTON_CLASS = 'twitter-img-download-button';
const observer = new MutationObserver(() => {
addButtonsToImages();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
function addButtonsToImages() {
const images = document.querySelectorAll('img');
for (const img of images) {
if (!img.src.includes('pbs.twimg.com/media/')) continue;
if (img.closest('div[data-shadow-host]')) continue;
if (img.dataset.hasDownloadButton) continue;
img.dataset.hasDownloadButton = true;
const host = document.createElement('div');
host.setAttribute('data-shadow-host', '1');
host.style.position = 'absolute';
host.style.top = '5px';
host.style.right = '5px';
host.style.zIndex = '9999';
host.style.pointerEvents = 'none';
const shadow = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
button {
background: rgba(0,0,0,0.6);
color: white;
border: none;
border-radius: 4px;
padding: 2px 6px;
font-size: 12px;
cursor: pointer;
pointer-events: auto;
}
button:hover {
background: rgba(0,0,0,0.85);
}
`;
const btn = document.createElement('button');
btn.textContent = '下載';
btn.className = BUTTON_CLASS;
btn.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
downloadImageBlob(img.src, img);
};
shadow.appendChild(style);
shadow.appendChild(btn);
const wrapper = document.createElement('div');
wrapper.style.position = 'relative';
wrapper.style.display = 'inline-block';
img.parentNode.insertBefore(wrapper, img);
wrapper.appendChild(img);
wrapper.appendChild(host);
}
}
async function downloadImageBlob(src, img) {
const finalUrl = new URL(src);
finalUrl.searchParams.set('format', 'png');
finalUrl.searchParams.set('name', '4096x4096');
const tweet = img.closest('article');
let filename = finalUrl.pathname.split('/').pop();
if (tweet) {
// const user = tweet.querySelector('div[data-testid="User-Name"] span')?.textContent || 'unknown';
// const text = tweet.querySelector('div[lang]')?.textContent?.slice(0, 30).replace(/[\\/:*?"<>|]/g, '_') || '';
const nameBlock = tweet.querySelector('div[data-testid="User-Name"]');
let handle = 'unknown';
if (nameBlock) {
const spans = nameBlock.querySelectorAll('span');
for (const s of spans) {
if (s.textContent.startsWith('@')) {
handle = s.textContent.replace('@', '');
break;
}
}
}
filename = `${handle}_${filename}`;
}
const response = await fetch(finalUrl.href);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${filename}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
const changeMedia = () => {
const format = 'png'
const name = '4096x4096'
if (location.href.includes('pbs.twimg.com/media') && location.href.includes('format=jpg')) {
const href = location.href;
const url = `${href.split('?')[0]}?format=${format}&name=${name}`
location.replace(url);
}
}
changeMedia();
})();