Adds a clean download option as first option to post/tweet dropdown ... menu, opens SaveTheVideo .. link to save to video script to fully automate download process after download clicked
当前为
// ==UserScript==
// @name X.com Video Downloader (Twitter)
// @namespace https://github.com/jayfantz
// @version 2.0
// @author jayfantz
// @description Adds a clean download option as first option to post/tweet dropdown ... menu, opens SaveTheVideo .. link to save to video script to fully automate download process after download clicked
// @match https://x.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const site = "https://www.savethevideo.com/downloader?url=";
const svg = `
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M12 3v10.586l3.293-3.293 1.414 1.414L12 17.414l-4.707-4.707 1.414-1.414L11 13.586V3h1z"/>
<path d="M5 19h14v2H5z"/>
</svg>`;
function resolveTweetUrl(menuItem) {
// normal: find <time> link inside the nearest article
let link = menuItem.closest('article')?.querySelector('time')?.parentElement?.href;
if (link) return link;
// timeline overlay: look for first tweet anchor inside same article
const article = menuItem.closest('article');
if (article) {
const anchor = article.querySelector('a[href*="/status/"]');
if (anchor) return anchor.href;
}
// pop-out player or embedded mode
const playable = document.querySelector('video')?.closest('article');
if (playable) {
const anchor = playable.querySelector('a[href*="/status/"]');
if (anchor) return anchor.href;
}
// fallback: current page
return location.href;
}
const observer = new MutationObserver(() => {
const menuItems = document.querySelectorAll('[role="menuitem"]:not(.dl-added)');
menuItems.forEach(item => {
const parent = item.closest('[role="menu"]');
if (parent && !parent.querySelector('.dl-download')) {
const dl = document.createElement('div');
dl.className = 'dl-download dl-added';
dl.style.cssText = `
display:flex;
align-items:center;
gap:10px;
padding:12px 16px;
cursor:pointer;
color:rgb(231,233,234);
font-family:"TwitterChirp",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
font-size:15px;
font-weight:600;
transition:background 0.15s ease;
`;
dl.innerHTML = `${svg}<span style="flex:1">Download video</span>`;
dl.addEventListener('mouseenter', () => dl.style.background = 'rgba(239,243,244,0.08)');
dl.addEventListener('mouseleave', () => dl.style.background = 'transparent');
dl.addEventListener('mouseenter', () => dl.style.background = 'rgba(255,255,255,0.1)');
dl.addEventListener('mouseleave', () => dl.style.background = 'transparent');
dl.addEventListener('click', e => {
e.stopPropagation();
// find the caret that opened this menu
const activeCaret = document.querySelector('[data-testid="caret"][aria-expanded="true"]');
const article = activeCaret?.closest('article');
const tweetUrl = article?.querySelector('a[href*="/status/"]')?.href || location.href;
if (!tweetUrl) {
alert('Could not locate tweet URL.');
return;
}
const url = site + encodeURIComponent(tweetUrl);
window.open(url, '_blank');
document.body.click(); // close menu
});
parent.appendChild(dl);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();