Greasy Fork is available in English.

推特获取原图

推特在新标签页打开图片自动原图

// ==UserScript==
// @name         推特获取原图
// @namespace    https://github.com/MuXia-0326/twitter-auto-original-picture
// @version      1.8
// @description  推特在新标签页打开图片自动原图
// @author       Mossia
// @icon         https://raw.githubusercontent.com/MuXia-0326/drawio/master/angri.png
// @match        https://pbs.twimg.com/*
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const copyUpdate = true;
  const share_url = '';

  //载入css样式
  const css = `/* From www.lingdaima.com */
  .twitter-Btn {
    position: relative;
    display: inline-block;
    padding: 10px;
    text-align: center;
    font-size: 18px;
    letter-spacing: 1px;
    text-decoration: none;
    color: rgb(29, 155, 240);
    background: transparent;
    cursor: pointer;
    transition: ease-out 0.5s;
    border: 2px solid rgb(29, 155, 240);
    border-radius: 10px;
    box-shadow: inset 0 0 0 0 rgb(29, 155, 240);
  }

  .twitter-Btn:hover {
    color: white;
    box-shadow: inset 0 -100px 0 0 rgb(29, 155, 240);
  }

  .twitter-Btn:active {
    transform: scale(0.9);
  }

  .twitter-Btn:hover svg {
    fill: white;
  }
  .twitter-Btn:active svg,
  .twitter-Btn svg {
    fill: rgb(29, 155, 240);
  }

  .Btn {
    position: absolute;
    top: 2px;
    right: 2px;
  }
  .svgClass {
    display: flex;
  }

  .share-btn {
    display:none;
  }
  `;

  let styleTag = document.createElement('style');
  styleTag.innerText = css;
  document.head.append(styleTag);

  // 获取当前页面的URL
  if (window.location.hostname === 'pbs.twimg.com') {
    let newUrl = replaceImageSizeName(window.location.href);
    if (newUrl !== window.location.href) {
      window.location.href = newUrl;
    }
  } else if (window.location.hostname === 'twitter.com' || window.location.hostname === 'x.com') {
    document.js_nsfw = setInterval(main, 100);
  }

  function main() {
    tweetAdd();
    imageDetailsAdd();
    copy();
    tweetAltAdd();
  }

  function tweetAltAdd() {
    let tweets = document.querySelectorAll('[data-testid="cellInnerDiv"]');

    for (let tweet of tweets) {
      let className = 'div.css-175oi2r.r-rki7wi.r-u8s1d.r-14fd9ze';

      let div = tweet.querySelector(className);
      if (div === null) {
        continue;
      }

      let parent = div.parentNode;
      let links = parent.querySelector('a');
      if (links === null) {
        continue;
      }

      let imageDiv = links.querySelector('img.css-9pa8cd');
      if (imageDiv === null) {
        continue;
      }

      let temp = [...new Set(baseSelectorAlt(parent, 'img.css-9pa8cd'))];
      let like = queryLikeBtn(tweet);

      for (let i = 0; i < temp.length; i++) {
        setAltBtn([temp[i]], like);
      }
    }
  }

  function copy() {
    if (copyUpdate) {
      // 替换复制按钮的url
      let firstChildDiv = document.querySelector('div[data-testid="Dropdown"] > div:first-child');

      // 确保第一个子元素是一个 div
      if (firstChildDiv) {
        firstChildDiv.addEventListener('click', function (e) {
          navigator.clipboard
            .readText()
            .then((text) => {
              // console.log('剪贴板的内容:', text);
              if (text.indexOf('fixupx') === -1) {
                // 修改剪贴板的内容
                GM_setClipboard(text.replace(/x/g, 'fixupx'), 'text');
              }
            })
            .catch((err) => {
              console.log('无法读取剪贴板的内容:', err);
            });
        });
      }
    }
  }

  function imageDetailsAdd() {
    // 图片详情页的按钮
    let classDetailsName = 'div[data-testid="swipe-to-dismiss"] div[aria-label="图像"]';

    let tempDetails = [...new Set(baseSelector(document, classDetailsName))];
    for (let i = 0; i < tempDetails.length; i++) {
      setDetailsBtn([tempDetails[i]]);
    }
  }

  function tweetAdd() {
    let tweets = document.querySelectorAll('[data-testid="cellInnerDiv"]');

    for (let tweet of tweets) {
      let className = 'div[aria-label="图像"][data-testid="tweetPhoto"]';

      let imageDiv = tweet.querySelector(className);
      if (imageDiv === null) {
        continue;
      }

      let temp = [...new Set(baseSelector(tweet, className))];
      let like = queryLikeBtn(tweet);

      for (let i = 0; i < temp.length; i++) {
        setBtn([temp[i]], like);
      }
    }
  }

  function queryLikeBtn(tweet) {
    let like = null;
    if (tweet.querySelector('div.css-175oi2r.r-16y2uox.r-1wbh5a2.r-1ny4l3l')) {
      let divs = tweet.querySelector('div.css-175oi2r.r-16y2uox.r-1wbh5a2.r-1ny4l3l');

      let childCount = divs.children.length;
      if (childCount === 3) {
        like = divs.children[2].children[4].querySelector('div').querySelector('div').children[2];
      } else if (childCount === 2) {
        like = divs.children[1].children[1].children[3].querySelector('div').querySelector('div').children[2];
      }
    } else if (tweet.querySelector('div.css-175oi2r.r-1iusvr4.r-16y2uox.r-1777fci.r-kzbkwu')) {
      like = tweet
        .querySelector('div.css-175oi2r.r-1iusvr4.r-16y2uox.r-1777fci.r-kzbkwu')
        .children[3].querySelector('div')
        .querySelector('div').children[2];
    }

    return like;
  }

  function baseSelector(parentEle, selector) {
    let items = parentEle.querySelectorAll(selector);
    return Array.from(items).filter((item) => {
      let node = getParentByNum(item, 5).querySelectorAll('div[data-nsfw]');
      return !(node && node.length > 0);
    });
  }

  function baseSelectorAlt(parentEle, selector) {
    let items = parentEle.querySelectorAll(selector);
    return Array.from(items).filter((item) => {
      let node = getParentByNum(item, 5).querySelectorAll('div[data-nsfw]');
      return !(node && node.length > 0);
    });
  }

  function setBtn(node, like) {
    for (let container of node) {
      let images = container.querySelectorAll('img');

      for (let image of images) {
        let imageUrl = image.getAttribute('src');
        let classText = image.getAttribute('class') + getRandomIntExclusive(10);

        let buttonHtml = getBtnHtml(classText);

        let parentElement = getParentByNum(image, 5);

        let newUrl = replaceImageSizeName(imageUrl);
        appendBtn(parentElement, newUrl, buttonHtml, classText, like);
      }
    }
  }

  function setAltBtn(node, like) {
    for (let images of node) {
      let imageUrl = images.getAttribute('src');
      let classText = images.getAttribute('class') + getRandomIntExclusive(10);

      let buttonHtml = getBtnHtml(classText);

      let parentElement = getParentByNum(images, 5);

      let newUrl = replaceImageSizeName(imageUrl);
      appendBtn(parentElement, newUrl, buttonHtml, classText, like);
    }
  }

  function setDetailsBtn(node) {
    for (let container of node) {
      // console.log(container);
      let images = container.querySelectorAll('img');

      for (let image of images) {
        let imageUrl = image.getAttribute('src');
        let classText = image.getAttribute('class') + getRandomIntExclusive(10);

        let buttonHtml = getBtnHtml(classText);

        let like = getParentByNum(container, 4).nextElementSibling.querySelector('div').querySelector('div').querySelector('div')
          .children[2];

        let newUrl = replaceImageSizeName(imageUrl);
        appendBtn(container, newUrl, buttonHtml, classText, like);
      }
    }
  }

  function getBtnHtml(classText) {
    const buttonHtml = `<div class="Btn">
                <button class="twitter-Btn" id="copy-${classText}">
                    <div class="svgClass">
                        <svg t="1694962361717" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5412" width="20" height="20">
                            <path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" p-id="5413" ></path>
                            <path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" p-id="5414" ></path>
                        </svg>
                    </div>
                </button>
                <button class="twitter-Btn" id="download-${classText}">
                    <div class="svgClass">
                        <svg t="1694962091616" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4129" id="mx_n_1694962091617" width="20" height="20">
                            <path d="M160 579.2a28.8 28.8 0 0 1 28.8 28.8v170.672c0 30.4 25.664 56.528 59.2 56.528h528c33.536 0 59.2-26.144 59.2-56.528V608a28.8 28.8 0 0 1 57.6 0v170.672c0 63.856-53.12 114.128-116.8 114.128h-528c-63.68 0-116.8-50.272-116.8-114.128V608a28.8 28.8 0 0 1 28.8-28.8z"  p-id="4130"></path><path d="M540.8 176l0 464a28.8 28.8 0 0 1-57.6 0L483.2 176a28.8 28.8 0 0 1 57.6 0z"  p-id="4131"></path>
                            <path d="M331.632 459.632a28.8 28.8 0 0 1 40.736 0l160 160a28.8 28.8 0 0 1-40.736 40.736l-160-160a28.8 28.8 0 0 1 0-40.736z" p-id="4132"></path><path d="M692.368 459.632a28.8 28.8 0 0 0-40.736 0l-160 160a28.8 28.8 0 0 0 40.736 40.736l160-160a28.8 28.8 0 0 0 0-40.736z" p-id="4133"></path>
                        </svg>
                    </div>
                </button>
                <button class="twitter-Btn share-btn" id="share-${classText}">
                    <div class="svgClass">
                        <svg t="1713618483987" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2790" width="20" height="20">
                            <path d="M720.020242 809.16812c0-49.233308 39.919175-89.151459 89.151459-89.151459s89.150436 39.918151 89.150436 89.151459c0 49.227168-39.918151 89.159646-89.150436 89.159646S720.020242 858.397335 720.020242 809.16812zM571.433112 214.824717c0-49.234331 39.919175-89.152483 89.151459-89.152483 49.234331 0 89.152483 39.918151 89.152483 89.152483 0 49.232285-39.918151 89.151459-89.152483 89.151459C611.352287 303.976176 571.433112 264.057001 571.433112 214.824717zM125.674792 675.441443c0-82.07018 66.530252-148.586107 148.585083-148.586107 82.056877 0 148.58713 66.515926 148.58713 148.586107 0 82.071204-66.531276 148.58713-148.58713 148.58713C192.205045 824.028573 125.674792 757.511623 125.674792 675.441443zM66.240145 675.441443c0 114.89375 93.142353 208.027917 208.019731 208.027917 81.402985 0 151.8525-46.752814 186.03809-114.870214l200.360284 35.288714-0.073678 5.28026c0 82.07632 66.531276 148.594293 148.58713 148.594293s148.586107-66.517973 148.586107-148.594293c0-82.072227-66.530252-148.586107-148.586107-148.586107-59.507302 0-110.815875 34.927487-134.554532 85.436858l-195.454554-34.403554c2.059915-11.738345 3.119037-23.839965 3.119037-36.174897 0-54.311976-20.822235-103.748922-54.892191-140.779304l180.740434-180.755784c16.309454 6.152117 33.983999 9.503445 52.454676 9.503445 82.056877 0 148.58713-66.514903 148.58713-148.586107 0-82.07018-66.530252-148.58713-148.58713-148.58713-82.055854 0-148.585083 66.51695-148.585083 148.58713 0 41.674145 17.165961 79.32772 44.792159 106.317421L381.013225 496.92056c-31.211862-18.71934-67.703985-29.499871-106.753349-29.499871C159.382499 467.421712 66.240145 560.549739 66.240145 675.441443z" p-id="2791"></path>
                        </svg>
                    </div>
                </button>
            </div>`;
    return buttonHtml;
  }

  function appendBtn(parentElement, newUrl, buttonHtml, classText, like) {
    // 创建按钮元素
    let button = document.createElement('div');
    button.setAttribute('data-nsfw', 'x');
    button.innerHTML = buttonHtml;

    // 按钮点击事件处理程序
    button.querySelector(`#copy-${classText}`).addEventListener('click', () => navigator.clipboard.writeText(newUrl));

    // 发起fetch请求获取图片内容
    button.querySelector(`#download-${classText}`).addEventListener('click', () => {
      // 点击喜欢按钮
      let likeDiv = like.querySelector('[data-testid="like"]');
      if (likeDiv) {
        likeDiv.click();
      }

      fetch(newUrl)
        .then(function (response) {
          if (response.ok) {
            return response.blob(); // 以Blob形式解析响应内容
          } else {
            throw new Error('下载失败');
          }
        })
        .then(function (imageBlob) {
          // 创建一个Blob URL,用于保存图片内容
          var imageUrl = URL.createObjectURL(imageBlob);

          let urlParams = new URL(newUrl);
          // 创建一个下载链接
          var downloadLink = document.createElement('a');
          downloadLink.href = imageUrl;
          downloadLink.download =
            urlParams.pathname.substring(urlParams.pathname.lastIndexOf('/') + 1) + '.' + urlParams.searchParams.get('format');

          // 模拟用户点击下载链接
          downloadLink.click();

          // 释放Blob URL以节省内存
          URL.revokeObjectURL(imageUrl);
        })
        .catch(function (error) {
          console.error('下载失败:', error);
        });
    });

    // 发起分享图片
    button.querySelector(`#share-${classText}`).addEventListener('click', () => {
      GM_xmlhttpRequest({
        method: 'POST',
        url: share_url, // 目标 URL
        headers: {
          'Content-Type': 'application/json',
        },
        data: JSON.stringify({ imageUrl: newUrl }),
        onload: function (response) {
          var responseData = JSON.parse(response.responseText);
          console.log('Received response:', responseData);
        },
        onerror: function (error) {
          console.error('Request failed:', error);
        },
      });
    });

    parentElement.appendChild(button);
  }

  function getParentByNum(element, number) {
    let ancestor = element;
    for (let i = 0; i < number; i++) {
      if (ancestor.parentElement) {
        ancestor = ancestor.parentElement;
      } else {
        break;
      }
    }
    return ancestor;
  }

  /**
   * @param {string} urlString
   */
  function replaceImageSizeName(urlString) {
    // 替换name参数的值为"orig"
    const url = new URL(urlString);
    url.searchParams.set('name', 'orig');
    return url.toString();
  }

  /**
   * @param {number} max
   */
  function getRandomIntExclusive(max) {
    return Math.floor(Math.random() * max);
  }
})();