Filter Reddit

Hide posts and comments containing specified keywords on Reddit

当前为 2024-11-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Filter Reddit
  3. // @namespace https://greasyfork.org/en/users/567951-stuart-saddler
  4. // @version 1.3
  5. // @description Hide posts and comments containing specified keywords on Reddit
  6. // @license MIT
  7. // @match *://www.reddit.com/*
  8. // @match *://old.reddit.com/*
  9. // @run-at document-end
  10. // @grant GM.getValue
  11. // @grant GM.setValue
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_addStyle
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_unregisterMenuCommand
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. let filteredCount = 0;
  23. let menuCommand = null;
  24. let processedPosts = new WeakSet();
  25. let keywordsArray = [];
  26.  
  27. // Cross-compatibility wrapper for GM functions
  28. const GM = {
  29. async getValue(name, defaultValue) {
  30. return typeof GM_getValue !== 'undefined'
  31. ? GM_getValue(name, defaultValue)
  32. : await GM.getValue(name, defaultValue);
  33. },
  34. async setValue(name, value) {
  35. if (typeof GM_setValue !== 'undefined') {
  36. GM_setValue(name, value);
  37. } else {
  38. await GM.setValue(name, value);
  39. }
  40. }
  41. };
  42.  
  43. const CSS = `
  44. .content-filtered {
  45. display: none !important;
  46. height: 0 !important;
  47. overflow: hidden !important;
  48. }
  49. .reddit-filter-dialog {
  50. position: fixed;
  51. top: 50%;
  52. left: 50%;
  53. transform: translate(-50%, -50%);
  54. background: white;
  55. padding: 20px;
  56. border-radius: 8px;
  57. z-index: 1000000;
  58. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  59. min-width: 300px;
  60. max-width: 500px;
  61. }
  62. .reddit-filter-dialog textarea {
  63. width: 100%;
  64. min-height: 200px;
  65. margin: 10px 0;
  66. padding: 8px;
  67. border: 1px solid #ccc;
  68. border-radius: 4px;
  69. font-family: monospace;
  70. }
  71. .reddit-filter-dialog button {
  72. padding: 8px 16px;
  73. margin: 0 5px;
  74. border: none;
  75. border-radius: 4px;
  76. cursor: pointer;
  77. }
  78. .reddit-filter-dialog .save-btn {
  79. background-color: #0079d3;
  80. color: white;
  81. }
  82. .reddit-filter-dialog .cancel-btn {
  83. background-color: #f2f2f2;
  84. }
  85. .reddit-filter-overlay {
  86. position: fixed;
  87. top: 0;
  88. left: 0;
  89. right: 0;
  90. bottom: 0;
  91. background: rgba(0,0,0,0.5);
  92. z-index: 999999;
  93. }
  94. `;
  95.  
  96. // Add CSS
  97. if (typeof GM_addStyle !== 'undefined') {
  98. GM_addStyle(CSS);
  99. } else {
  100. const style = document.createElement('style');
  101. style.textContent = CSS;
  102. document.head.appendChild(style);
  103. }
  104.  
  105. function getKeywords() {
  106. return GM_getValue('filterKeywords', []);
  107. }
  108.  
  109. async function showConfig() {
  110. const overlay = document.createElement('div');
  111. overlay.className = 'reddit-filter-overlay';
  112.  
  113. const dialog = document.createElement('div');
  114. dialog.className = 'reddit-filter-dialog';
  115. dialog.innerHTML = `
  116. <h2 style="margin-top: 0;">Reddit Filter Keywords</h2>
  117. <p>Enter keywords one per line:</p>
  118. <textarea spellcheck="false">${keywordsArray.join('\n')}</textarea>
  119. <div style="text-align: right;">
  120. <button class="cancel-btn">Cancel</button>
  121. <button class="save-btn">Save</button>
  122. </div>
  123. `;
  124.  
  125. document.body.appendChild(overlay);
  126. document.body.appendChild(dialog);
  127.  
  128. const closeDialog = () => {
  129. dialog.remove();
  130. overlay.remove();
  131. };
  132.  
  133. dialog.querySelector('.save-btn').addEventListener('click', async () => {
  134. const newKeywords = dialog.querySelector('textarea').value
  135. .split('\n')
  136. .map(k => k.trim())
  137. .filter(k => k.length > 0);
  138. await GM.setValue('filterKeywords', newKeywords);
  139. closeDialog();
  140. location.reload();
  141. });
  142.  
  143. dialog.querySelector('.cancel-btn').addEventListener('click', closeDialog);
  144. overlay.addEventListener('click', closeDialog);
  145. }
  146.  
  147. function updateCounter() {
  148. if (menuCommand) {
  149. GM_unregisterMenuCommand(menuCommand);
  150. }
  151. menuCommand = GM_registerMenuCommand(
  152. `Configure Filter Keywords (${filteredCount} blocked)`,
  153. showConfig
  154. );
  155. }
  156.  
  157. function processPost(post) {
  158. if (!post || processedPosts.has(post)) return;
  159. processedPosts.add(post);
  160.  
  161. const postContent = [
  162. post.textContent,
  163. ...Array.from(post.querySelectorAll('h1, h2, h3, p, a[href], [role="heading"]'))
  164. .map(el => el.textContent)
  165. ].join(' ').toLowerCase();
  166.  
  167. if (keywordsArray.some(keyword => postContent.includes(keyword.toLowerCase()))) {
  168. post.classList.add('content-filtered');
  169. const parentArticle = post.closest('article, div[data-testid="post-container"]');
  170. if (parentArticle) {
  171. parentArticle.classList.add('content-filtered');
  172. }
  173. filteredCount++;
  174. updateCounter();
  175. }
  176. }
  177.  
  178. async function init() {
  179. keywordsArray = await GM.getValue('filterKeywords', []);
  180.  
  181. // Initialize menu
  182. updateCounter();
  183.  
  184. const observer = new MutationObserver((mutations) => {
  185. for (const mutation of mutations) {
  186. for (const node of mutation.addedNodes) {
  187. if (node.nodeType === Node.ELEMENT_NODE) {
  188. if (node.matches('article, div[data-testid="post-container"], shreddit-post')) {
  189. processPost(node);
  190. }
  191. node.querySelectorAll('article, div[data-testid="post-container"], shreddit-post')
  192. .forEach(processPost);
  193. }
  194. }
  195. }
  196. });
  197.  
  198. observer.observe(document.body, {
  199. childList: true,
  200. subtree: true
  201. });
  202.  
  203. // Initial processing
  204. document.querySelectorAll('article, div[data-testid="post-container"], shreddit-post')
  205. .forEach(processPost);
  206. }
  207.  
  208. // Ensure DOM is ready before initializing
  209. if (document.readyState === 'loading') {
  210. document.addEventListener('DOMContentLoaded', init);
  211. } else {
  212. init();
  213. }
  214. })();