Twitterᴾˡᵘˢ

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

נכון ליום 05-12-2023. ראה הגרסה האחרונה.

  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.5
  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. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  15. // @match https://twitter.com/*
  16. // @match https://mobile.twitter.com/*
  17. // @match https://pbs.twimg.com/media/*
  18. // @license AGPL-3.0-or-later
  19. // @grant GM_setValue
  20. // @grant GM_getValue
  21. // @grant GM_addStyle
  22. // @grant GM_registerMenuCommand
  23. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  24. // @compatible Chrome
  25. // @compatible Firefox
  26. // ==/UserScript==
  27. // Hide the post if the hashtag exceeds the set number. (If set to 0, it will not be enabled)
  28. if (GM_getValue('MAX_HASHTAGS') == undefined) { GM_setValue('MAX_HASHTAGS', 20); }
  29. // Hide the post if it contains the following hashtag. (Please include "#" and separate using commas)
  30. if (GM_getValue('OUT_HASHTAGS') == undefined) { GM_setValue('OUT_HASHTAGS', '#tag1,#tag2'); }
  31. // Change OUT_HASHTAGS type to string
  32. if (typeof GM_getValue('OUT_HASHTAGS') == 'object') { GM_setValue('OUT_HASHTAGS', GM_getValue('OUT_HASHTAGS').join(',')); }
  33. // Custom style.
  34. GM_addStyle(`
  35. iframe#twitter_plus_setting {
  36. max-width: 300px !important;
  37. max-height: 300px !important;
  38. }`);
  39.  
  40. (function () {
  41. 'use strict';
  42.  
  43. const getOriginUrl = (imgUrl) => {
  44. let match = imgUrl.match(/https:\/\/(pbs\.twimg\.com\/media\/[a-zA-Z0-9\-\_]+)(\?format=|.)(jpg|jpeg|png|webp)/);
  45. if (!match) return false;
  46. // webp change to jpg
  47. if (match[3] == 'webp') match[3] = 'jpg';
  48. // change it to obtain the original quality.
  49. if (match[2] == '?format=' || !/name=orig/.test(imgUrl)) {
  50. return `https://${match[1]}.${match[3]}?name=orig`
  51. } else {
  52. return false;
  53. }
  54. }
  55. const URL = window.location.href;
  56. // browsing an image URL
  57. if (URL.includes('twimg.com')) {
  58. let originUrl = getOriginUrl(URL);
  59. if (originUrl) window.location.replace(originUrl);
  60. }
  61. // if browsing tweets, activate the observer.
  62. if (URL.includes('twitter.com')) {
  63. const rootmatch = document.evaluate('//div[@id="react-root"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  64. const rootnode = rootmatch.singleNodeValue;
  65. const MAX_HASHTAGS = GM_getValue('MAX_HASHTAGS');
  66. const OUT_HASHTAGS = GM_getValue('OUT_HASHTAGS').split(',');
  67. const checkElement = (ele) => {
  68. return [
  69. ele.dataset.testid == 'tweet',
  70. ele.dataset.testid == 'tweetPhoto',
  71. ele.className == 'css-175oi2r r-1pi2tsx r-u8s1d r-13qz1uu',
  72. ].some(item => item);
  73. }
  74. if (rootnode) {
  75. const callback = (mutationsList, observer) => {
  76. for (const mutation of mutationsList) {
  77. const target = mutation.target;
  78. if (!checkElement(target)) continue;
  79. // only the article node needs to be checked for spam or ads.
  80. if (target.nodeName == 'ARTICLE') {
  81. try {
  82. const hashtags = Array.from(target.querySelectorAll('a[href^="/hashtag/"]'), tag => tag.textContent);
  83. // exceeding the numbers of hashtags.
  84. if (MAX_HASHTAGS > 0 && hashtags.length >= MAX_HASHTAGS) throw target;
  85. // containing specified hashtags.
  86. if (hashtags.some(tag => OUT_HASHTAGS.find(item => item == tag))) throw target;
  87. // ads.
  88. if (target.querySelector('svg.r-1q142lx')) throw target;
  89. } catch (e) {
  90. // hidden tweet
  91. if (e) e.closest('div[data-testid="cellInnerDiv"]').style.display = 'none';
  92. continue;
  93. }
  94. }
  95. const images = target.querySelectorAll('img');
  96. if (!images.length) continue;
  97. // tweets image
  98. for (const image of images) {
  99. let originUrl = getOriginUrl(image.src);
  100. if (originUrl) image.src = originUrl;
  101. continue;
  102. }
  103. }
  104. }
  105. const observer = new MutationObserver(callback);
  106. // start observe
  107. observer.observe(document.body, {
  108. attributes: true,
  109. childList: true,
  110. subtree: true
  111. });
  112. }
  113. }
  114. })();
  115.  
  116. GM_registerMenuCommand('Setting', () => config.open());
  117.  
  118. const config = new GM_config({
  119. 'id': 'twitter_plus_setting',
  120. 'css': `
  121. #twitter_plus_setting_wrapper {
  122. height: 100%;
  123. display: flex;
  124. flex-direction: column;
  125. }
  126. #twitter_plus_setting_section_0 {
  127. flex: 1;
  128. }
  129. #twitter_plus_setting_buttons_holder {
  130. text-align: center;
  131. }
  132. .config_var {
  133. display: flex;
  134. flex-direction: column;
  135. margin-bottom: 1rem !important;
  136. }
  137. `,
  138. 'title': 'Spam tweets',
  139. 'fields': {
  140. 'MAX_HASHTAGS': {
  141. 'label': 'How many hashtags to hide?',
  142. 'type': 'number',
  143. 'title': 'input 0 to disable',
  144. 'min': 0,
  145. 'max': 100,
  146. 'default': 20,
  147. },
  148. 'OUT_HASHTAGS': {
  149. 'label': 'Which tags must be hidden?',
  150. 'type': 'textarea',
  151. 'title': 'Must include # and separated by commas.',
  152. 'default': '#tag1,#tag2',
  153. }
  154. },
  155. 'events': {
  156. 'init': () => {
  157. if (GM_getValue('MAX_HASHTAGS')) { config.set('MAX_HASHTAGS', GM_getValue('MAX_HASHTAGS')) }
  158. if (GM_getValue('OUT_HASHTAGS')) { config.set('OUT_HASHTAGS', GM_getValue('OUT_HASHTAGS')) }
  159. },
  160. 'save': () => {
  161. GM_setValue('OUT_HASHTAGS', config.get('OUT_HASHTAGS'));
  162. GM_setValue('MAX_HASHTAGS', config.get('MAX_HASHTAGS'));
  163. config.close();
  164. }
  165. }
  166. });