Twitterᴾˡᵘˢ

Enhance the Twitter user experience by loading images in their original quality and removing ads and spam tweets.

Versão de: 21/05/2024. Veja: a última versão.

  1. // ==UserScript==
  2. // @name Twitterᴾˡᵘˢ
  3. // @name:zh-TW Twitterᴾˡᵘˢ
  4. // @name:zh-CN Twitterᴾˡᵘˢ
  5. // @name:ja Twitterᴾˡᵘˢ
  6. // @namespace https://greasyfork.org
  7. // @version 0.3.7
  8. // @description Enhance the Twitter user experience by loading images in their original quality and removing ads and spam tweets.
  9. // @description:zh-TW 增強Twitter使用體驗。讀取原始畫質的圖片,移除廣告與垃圾推文。
  10. // @description:zh-CN 增强Twitter使用体验。读取原始画质的图片,移除广告与垃圾推文。
  11. // @description:ja Twitterの利用体験を向上させます。元の高画質で画像をロードします、広告や迷惑なツイートを削除します。
  12. // @author Pixmi
  13. // @homepage https://github.com/Pixmi/twitter-plus
  14. // @supportURL https://github.com/Pixmi/twitter-plus/issues
  15. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  16. // @match https://twitter.com/*
  17. // @match https://mobile.twitter.com/*
  18. // @match https://pbs.twimg.com/media/*
  19. // @license MPL-2.0
  20. // @grant GM_setValue
  21. // @grant GM_getValue
  22. // @grant GM_addStyle
  23. // @grant GM_registerMenuCommand
  24. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  25. // @compatible Chrome
  26. // @compatible Firefox
  27. // ==/UserScript==
  28. // Hide the post if the hashtag exceeds the set number. (If set to 0, it will not be enabled)
  29. if (GM_getValue('MAX_HASHTAGS') == undefined) { GM_setValue('MAX_HASHTAGS', 20); }
  30. // Hide the post if it contains the following hashtag. (Please include "#" and separate using commas)
  31. if (GM_getValue('OUT_HASHTAGS') == undefined) { GM_setValue('OUT_HASHTAGS', '#tag1,#tag2'); }
  32. // Change OUT_HASHTAGS type to string
  33. if (typeof GM_getValue('OUT_HASHTAGS') == 'object') { GM_setValue('OUT_HASHTAGS', GM_getValue('OUT_HASHTAGS').join(',')); }
  34. // Custom style.
  35. GM_addStyle(`
  36. iframe#twitter_plus_setting {
  37. max-width: 300px !important;
  38. max-height: 300px !important;
  39. }`);
  40.  
  41. (function () {
  42. 'use strict';
  43.  
  44. const getOriginUrl = (imgUrl) => {
  45. let match = imgUrl.match(/https:\/\/(pbs\.twimg\.com\/media\/[a-zA-Z0-9\-\_]+)(\?format=|.)(jpg|jpeg|png|webp)/);
  46. if (!match) return false;
  47. // webp change to jpg
  48. if (match[3] == 'webp') match[3] = 'jpg';
  49. // change it to obtain the original quality.
  50. if (match[2] == '?format=' || !/name=orig/.test(imgUrl)) {
  51. return `https://${match[1]}.${match[3]}?name=orig`
  52. } else {
  53. return false;
  54. }
  55. }
  56. const URL = window.location.href;
  57. // browsing an image URL
  58. if (URL.includes('twimg.com')) {
  59. let originUrl = getOriginUrl(URL);
  60. if (originUrl) window.location.replace(originUrl);
  61. }
  62. // if browsing tweets, activate the observer.
  63. if (URL.includes('twitter.com')) {
  64. const rootmatch = document.evaluate('//div[@id="react-root"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  65. const rootnode = rootmatch.singleNodeValue;
  66. const MAX_HASHTAGS = GM_getValue('MAX_HASHTAGS');
  67. const OUT_HASHTAGS = GM_getValue('OUT_HASHTAGS').split(',');
  68. const checkElement = (ele) => {
  69. return [
  70. ele.dataset.testid == 'tweet',
  71. ele.dataset.testid == 'tweetPhoto',
  72. ele.className == 'css-175oi2r r-1pi2tsx r-u8s1d r-13qz1uu',
  73. ].some(item => item);
  74. }
  75. if (rootnode) {
  76. const callback = (mutationsList, observer) => {
  77. for (const mutation of mutationsList) {
  78. const target = mutation.target;
  79. if (!checkElement(target)) continue;
  80. // only the article node needs to be checked for spam or ads.
  81. if (target.nodeName == 'ARTICLE') {
  82. try {
  83. const hashtags = Array.from(target.querySelectorAll('a[href^="/hashtag/"]'), tag => tag.textContent);
  84. // exceeding the numbers of hashtags.
  85. if (MAX_HASHTAGS > 0 && hashtags.length >= MAX_HASHTAGS) throw target;
  86. // containing specified hashtags.
  87. if (hashtags.some(tag => OUT_HASHTAGS.find(item => item == tag))) throw target;
  88. // ads.
  89. if (target.querySelector('svg.r-1q142lx')) throw target;
  90. } catch (e) {
  91. // hidden tweet
  92. if (e) e.closest('div[data-testid="cellInnerDiv"]').style.display = 'none';
  93. continue;
  94. }
  95. }
  96. const images = target.querySelectorAll('img');
  97. if (!images.length) continue;
  98. // tweets image
  99. for (const image of images) {
  100. let originUrl = getOriginUrl(image.src);
  101. if (originUrl) image.src = originUrl;
  102. continue;
  103. }
  104. }
  105. }
  106. const observer = new MutationObserver(callback);
  107. // start observe
  108. observer.observe(document.body, {
  109. attributes: true,
  110. childList: true,
  111. subtree: true
  112. });
  113. }
  114. }
  115. })();
  116.  
  117. GM_registerMenuCommand('Setting', () => config.open());
  118.  
  119. const config = new GM_config({
  120. 'id': 'twitter_plus_setting',
  121. 'css': `
  122. #twitter_plus_setting_wrapper {
  123. height: 100%;
  124. display: flex;
  125. flex-direction: column;
  126. }
  127. #twitter_plus_setting_section_0 {
  128. flex: 1;
  129. }
  130. #twitter_plus_setting_buttons_holder {
  131. text-align: center;
  132. }
  133. .config_var {
  134. display: flex;
  135. flex-direction: column;
  136. margin-bottom: 1rem !important;
  137. }
  138. `,
  139. 'title': 'Remove Spam',
  140. 'fields': {
  141. 'MAX_HASHTAGS': {
  142. 'label': 'When exceeding how many hashtags?',
  143. 'type': 'number',
  144. 'title': 'input 0 to disable',
  145. 'min': 0,
  146. 'max': 100,
  147. 'default': 20,
  148. },
  149. 'OUT_HASHTAGS': {
  150. 'label': 'When containing which hashtags?',
  151. 'type': 'textarea',
  152. 'title': 'Must include # and separated by commas.',
  153. 'default': '#tag1,#tag2',
  154. }
  155. },
  156. 'events': {
  157. 'init': () => {
  158. if (GM_getValue('MAX_HASHTAGS')) { config.set('MAX_HASHTAGS', GM_getValue('MAX_HASHTAGS')) }
  159. if (GM_getValue('OUT_HASHTAGS')) { config.set('OUT_HASHTAGS', GM_getValue('OUT_HASHTAGS')) }
  160. },
  161. 'save': () => {
  162. GM_setValue('OUT_HASHTAGS', config.get('OUT_HASHTAGS'));
  163. GM_setValue('MAX_HASHTAGS', config.get('MAX_HASHTAGS'));
  164. config.close();
  165. }
  166. }
  167. });