Greasy Fork is available in English.

Twitterᴾˡᵘˢ (View Twitter Origin Images)

Enhance Twitter user experience. Load images in original quality, allow download video links from tweets, hiding tweets that contain specific hashtags or exceed the maximum limit.

As of 2023-03-15. See the latest version.

  1. // ==UserScript==
  2. // @name Twitterᴾˡᵘˢ (View Twitter Origin Images)
  3. // @name:zh-TW Twitterᴾˡᵘˢ (檢視Twitter原始圖檔)
  4. // @name:zh-CN Twitterᴾˡᵘˢ (检视Twitter原始图档)
  5. // @namespace https://greasyfork.org
  6. // @version 0.2.0
  7. // @description Enhance Twitter user experience. Load images in original quality, allow download video links from tweets, hiding tweets that contain specific hashtags or exceed the maximum limit.
  8. // @description:zh-TW 增強Twitter使用者體驗。讀取原始畫質的圖片,允許從推文下載影片鏈結,隱藏包含特定Hashtag或超過最大限制的推文。
  9. // @description:zh-CN 增强Twitter使用者体验。读取原始画质的图片,允许从推文下载影片链结,隐藏包含特定Hashtag或超过最大限制的推文。
  10. // @author Pixmi
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  12. // @match https://twitter.com/*
  13. // @match https://pbs.twimg.com/media/*
  14. // @license MIT
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_addStyle
  18. // ==/UserScript==
  19. // Hide the post if the hashtag exceeds the set number. (If set to 0, it will not be enabled)
  20. GM_setValue('MAX_HASHTAGS', 20);
  21. // Hide the post if it contains the following hashtag. (Please include "#" and separate using commas)
  22. GM_setValue('OUT_HASHTAGS', ['#tag1', '#tag2']);
  23. // custom style.
  24. GM_addStyle(`
  25. .video-link-icon:hover {
  26. color: rgba(240, 181, 5, 1) !important;
  27. }
  28. .video-link-icon:hover::after {
  29. background: rgba(240, 181, 5, .1);
  30. border-radius: 50%;
  31. content: " ";
  32. width: 38.5px;
  33. height: 38.5px;
  34. position: absolute;
  35. top: 50%;
  36. left: 50%;
  37. transform: translate(-50%, -50%);
  38. }
  39. .video-link-icon > svg {
  40. width: 1.2em;
  41. height: 1.2em
  42. }`);
  43.  
  44. (function () {
  45. 'use strict';
  46. let URL = window.location.href;
  47. // If browsing an image URL, change it to obtain the original quality.
  48. if (URL.match(/https:\/\/pbs\.twimg\.com\/media\/([a-zA-Z0-9\-\_]+)(\?format=|.)(jpg|jpeg|png)/) && !URL.includes('?name=orig')) {
  49. if (URL.indexOf('?format=') > 0) {
  50. URL = URL.replace('?format=', '.');
  51. }
  52. if (URL.match(/\&name=(\w+)/)) {
  53. URL = URL.replace(/\&name=(\w+)/g, '?name=orig');
  54. } else {
  55. URL = `${URL}?name=orig`;
  56. }
  57. window.location.replace(URL);
  58. }
  59. // If browsing tweets, activate the observer.
  60. if (URL.includes('twitter.com')) {
  61. const rootmatch = document.evaluate('//div[@id="react-root"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  62. const rootnode = rootmatch.singleNodeValue;
  63. const MAX_HASHTAGS = GM_getValue('MAX_HASHTAGS');
  64. const OUT_HASHTAGS = GM_getValue('OUT_HASHTAGS');
  65. if (rootnode) {
  66. let getVidoLink = (element) => {
  67. let react = Object.entries(element).find(el => el[0].startsWith("__reactFiber"))[1];
  68. if (!react) return false;
  69. let tweet = react.memoizedProps.children.filter(el => (el || {})._owner).map(el => el._owner.memoizedProps.focalTweet).filter(el => el)[0];
  70. if (!tweet) return false;
  71. let video = tweet.extended_entities.media[0].video_info.variants.filter(v => v.content_type == "video/mp4").sort((a,b) => b.bitrate - a.bitrate)[0].url.replace(new RegExp("\\?tag=.*"), "");
  72. if (!video) return false;
  73. return video;
  74. };
  75.  
  76. let createPopupLink = (link, group) => {
  77. let button = document.createElement('a');
  78. button.classList.add('video-link-icon', 'css-18t94o4', 'css-1dbjc4n', 'r-1777fci', 'r-bt1l66', 'r-1ny4l3l', 'r-bztko3', 'r-lrvibr', 'r-1bwzh9t', 'r-115tad6', 'r-14j79pv');
  79. button.title = 'Open the video link or right-click to save the file.';
  80. button.target= '_blank';
  81. button.href = link;
  82. button.innerHTML = '<svg class="r-4qtqp9 r-yyyyoo r-dnmrzs r-bnwqim r-1plcrui r-lrvibr" viewBox="0 0 512 512" aria-hidden="true"><path fill="currentColor" d="M432,288H416a16,16,0,0,0-16,16V458a6,6,0,0,1-6,6H54a6,6,0,0,1-6-6V118a6,6,0,0,1,6-6H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V304A16,16,0,0,0,432,288ZM500,0H364a12,12,0,0,0-8.48,20.48l48.19,48.21L131.51,340.89a12,12,0,0,0,0,17l22.63,22.63a12,12,0,0,0,17,0l272.2-272.21,48.21,48.2A12,12,0,0,0,512,148V12A12,12,0,0,0,500,0Z"/></svg>';
  83. let colorItem = group.firstChild.querySelector('div[style*="color"]') || false;
  84. if (colorItem) button.style.color = colorItem.style.color;
  85. return button;
  86. };
  87.  
  88. let callback = (mutationsList, observer) => {
  89. for (let mutation of mutationsList) {
  90. let target = mutation.target;
  91. if (target.className.includes('css-1dbjc4n')) {
  92. let hashtags = Array.from(target.querySelectorAll('.css-901oao > .r-18u37iz'), tag => tag.textContent);
  93. let hideCheck = [];
  94. if (hashtags.length && target.nodeName == 'ARTICLE') {
  95. if (MAX_HASHTAGS > 0) hideCheck.push(hashtags.length >= MAX_HASHTAGS);
  96. hideCheck.push(hashtags.some(tag => OUT_HASHTAGS.find(item => item == tag)));
  97. if (hideCheck.some(item => item === true)) {
  98. target.closest('div[data-testid="cellInnerDiv"] > div').style.display = 'none';
  99. target.remove();
  100. continue;
  101. }
  102. }
  103. let images = target.getElementsByTagName('img');
  104. for (let i = 0; i < images.length; i++) {
  105. let imgSrc = images[i].src;
  106. if (imgSrc.includes('https://pbs.twimg.com/media/') && !imgSrc.match(/name=orig/)) {
  107. images[i].src = imgSrc.replace('?format=', '.').replace(/&name=(\w+)/g, '?name=orig');
  108. } else
  109. if (imgSrc.includes('video_thumb') && URL.includes('/status/')) {
  110. if ('link' in images[i].dataset) continue;
  111. let group = target.closest('article').querySelector('div[id^="id"][role="group"]');
  112. if (!group || group.lastChild.nodeName == 'A') continue;
  113. let link = getVidoLink(target.closest('section[role="region"]').parentElement);
  114. if (!link) continue;
  115. images[i].dataset.link = link;
  116. group.append(createPopupLink(link, group));
  117. }
  118. }
  119. }
  120. }
  121. };
  122. const observeConfig = {
  123. attributes: true,
  124. childList: true,
  125. subtree: true
  126. };
  127. const observer = new MutationObserver(callback);
  128.  
  129. observer.observe(document.body, observeConfig);
  130. }
  131. }
  132. })();