Greasy Fork

来自缓存

Greasy Fork is available in English.

Twitter/X 视频下载器 (Premium)

在 Twitter/X 视频下方添加下载按钮,点击直接跳转 Twisaver 进行下载。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitter/X Video Downloader (Premium)
// @name:ja      Twitter/X 動画 ダウンローダー (Premium)
// @name:zh-CN   Twitter/X 视频下载器 (Premium)
// @namespace    https://twisaver.net/
// @version      1.0
// @description  Add a Download button under Twitter/X videos that opens Twisaver with the tweet URL.
// @description:ja  ワンクリックでTwitter/Xの動画やGIFを保存します。Twisaverで高速・高画質にダウンロード。
// @description:zh-CN  在 Twitter/X 视频下方添加下载按钮,点击直接跳转 Twisaver 进行下载。
// @author       Twisaver
// @license      MIT
// @match        *://twitter.com/*
// @match        *://x.com/*
// @icon         https://twisaver.net/favicon.ico
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(() => {
  'use strict';

  const STYLE_ID = 'twisaver-userscript-style';
  const BUTTON_CLASS = 'twisaver-download-btn';
  const ICON_CLASS = 'twisaver-download-icon';
  const TOOLTIP_CLASS = 'twisaver-download-tooltip';
  const UTM_QUERY = '&utm_source=userscript&utm_medium=twitter_button&utm_campaign=extension';
  const SCAN_DEBOUNCE_MS = 300;

  function ensureStyle() {
    if (document.getElementById(STYLE_ID)) return;

    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = `
      .${BUTTON_CLASS} {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        height: 32px;
        padding: 0 12px;
        border-radius: 9999px;
        color: rgb(83, 100, 113);
        font-size: 13px;
        line-height: 16px;
        cursor: pointer;
        user-select: none;
        position: relative;
        z-index: 3;
        transition: background-color 120ms ease, color 120ms ease;
      }
      .${BUTTON_CLASS}:hover {
        background-color: rgba(29, 155, 240, 0.12);
        color: rgb(29, 155, 240);
      }
      .${ICON_CLASS} {
        width: 16px;
        height: 16px;
        border-radius: 4px;
        flex: 0 0 auto;
      }
      .${TOOLTIP_CLASS} {
        position: absolute;
        left: 50%;
        top: calc(100% + 6px);
        transform: translateX(-50%);
        white-space: nowrap;
        background: rgba(29, 155, 240, 0.16);
        color: rgb(29, 155, 240);
        border: 1px solid rgba(29, 155, 240, 0.35);
        padding: 6px 10px;
        border-radius: 10px;
        font-size: 12px;
        line-height: 14px;
        opacity: 0;
        visibility: hidden;
        pointer-events: none;
        transition: opacity 120ms ease, transform 120ms ease;
        z-index: 9999;
      }
      .${BUTTON_CLASS}:hover .${TOOLTIP_CLASS} {
        opacity: 1;
        visibility: visible;
        transform: translateX(-50%) translateY(2px);
      }
    `;
    document.head.appendChild(style);
  }

  function getTweetUrl(article) {
    // 只抓取包含 /status/ 的推文链接
    const anchor = article.querySelector('a[href*="/status/"]');
    if (!anchor) return null;

    const href = anchor.getAttribute('href');
    if (!href) return null;

    return href.startsWith('http') ? href : `${location.origin}${href}`;
  }

  function isVideoTweet(article) {
    // 仅对包含 video 标签的推文加按钮
    return Boolean(article.querySelector('video'));
  }

  function buildTargetUrl(tweetUrl) {
    const encoded = encodeURIComponent(tweetUrl);
    return `https://twisaver.net/?url=${encoded}${UTM_QUERY}`;
  }

  function onActivate(article) {
    const tweetUrl = getTweetUrl(article) || location.href;
    const targetUrl = buildTargetUrl(tweetUrl);
    window.open(targetUrl, '_blank', 'noopener');
  }

  function createButton(article) {
    const button = document.createElement('div');
    button.className = BUTTON_CLASS;
    button.setAttribute('role', 'button');
    button.setAttribute('tabindex', '0');
    button.setAttribute('aria-label', 'Download');

    const icon = document.createElement('img');
    icon.className = ICON_CLASS;
    const fallbackIcon =
      'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="%23536' +
      '471" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M12 3v12"/><path ' +
      'd="M7 11l5 5 5-5"/><path d="M5 21h14"/></svg>';
    // 优先使用 favicon,加载失败时回退到内联 SVG
    icon.src = 'https://twisaver.net/favicon.ico';
    icon.addEventListener(
      'error',
      () => {
        icon.src = fallbackIcon;
      },
      { once: true }
    );
    icon.alt = '';

    const label = document.createElement('span');
    label.textContent = 'Download';

    const tooltip = document.createElement('span');
    tooltip.className = TOOLTIP_CLASS;
    tooltip.textContent = 'Download via Twisaver.net (Fast & Free)';

    button.appendChild(icon);
    button.appendChild(label);
    button.appendChild(tooltip);

    button.addEventListener('click', (event) => {
      event.stopPropagation();
      onActivate(article);
    });

    button.addEventListener('keydown', (event) => {
      if (event.key === 'Enter' || event.key === ' ') {
        event.preventDefault();
        onActivate(article);
      }
    });

    return button;
  }

  function insertButton(article) {
    const actionGroup = article.querySelector('div[role="group"]');
    if (!actionGroup) return;
    if (actionGroup.querySelector(`.${BUTTON_CLASS}`)) return;

    // 让父容器可溢出显示,避免气泡被裁剪
    article.style.overflow = 'visible';
    actionGroup.style.overflow = 'visible';
    actionGroup.style.position = 'relative';

    const button = createButton(article);
    actionGroup.appendChild(button);
  }

  function scanTweets() {
    ensureStyle();

    const articles = document.querySelectorAll('article');
    for (const article of articles) {
      if (!isVideoTweet(article)) continue;
      insertButton(article);
    }
  }

  let scanTimer = 0;
  function scheduleScan() {
    if (scanTimer) return;
    scanTimer = window.setTimeout(() => {
      scanTimer = 0;
      scanTweets();
    }, SCAN_DEBOUNCE_MS);
  }

  const observer = new MutationObserver(() => {
    // SPA + 无限滚动:使用防抖避免高频触发
    scheduleScan();
  });

  function start() {
    scanTweets();
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start, { once: true });
  } else {
    start();
  }
})();