Greasy Fork 支持简体中文。

FreshRSS Duplicate Filter

Mark as read and hide older articles in the FreshRSS feed list that have the same title, URL and content within a category or feed.

  1. // ==UserScript==
  2. // @name FreshRSS Duplicate Filter
  3. // @namespace https://github.com/hiroki-miya
  4. // @version 1.0.3
  5. // @description Mark as read and hide older articles in the FreshRSS feed list that have the same title, URL and content within a category or feed.
  6. // @author hiroki-miya
  7. // @license MIT
  8. // @match https://freshrss.example.net/*
  9. // @grant GM_addStyle
  10. // @grant GM_getValue
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_setValue
  13. // @grant GM_xmlhttpRequest
  14. // @run-at document-idle
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // Default settings
  21. const DEFAULT_CATEGORY_LIST = [];
  22. const DEFAULT_CHECK_LIMIT = 100;
  23.  
  24. // Load saved settings
  25. let selectedCategories = GM_getValue('selectedCategories', DEFAULT_CATEGORY_LIST);
  26. let checkLimit = GM_getValue('checkLimit', DEFAULT_CHECK_LIMIT);
  27.  
  28. // Add styles
  29. GM_addStyle(`
  30. #freshrss-duplicate-filter {
  31. position: fixed;
  32. top: 50%;
  33. left: 50%;
  34. transform: translate(-50%, -50%);
  35. z-index: 10000;
  36. background-color: white;
  37. border: 1px solid black;
  38. padding: 10px;
  39. width: max-content;
  40. }
  41. #freshrss-duplicate-filter > h2 {
  42. box-shadow: inset 0 0 0 0.5px black;
  43. padding: 5px 10px;
  44. text-align: center;
  45. cursor: move;
  46. }
  47. #freshrss-duplicate-filter > h4 {
  48. margin-top: 0;
  49. }
  50. #fdfs-categories {
  51. margin-bottom: 10px;
  52. max-height: 60vh;
  53. overflow-y: auto;
  54. }
  55. `);
  56.  
  57. // Settings screen
  58. function showSettings() {
  59. const categories = Array.from(document.querySelectorAll('#sidebar a > span.title')).map(cat => cat.innerText);
  60. const selected = selectedCategories || [];
  61.  
  62. let categoryOptions = categories.map(cat => {
  63. const checked = selected.includes(cat) ? 'checked' : '';
  64. return `<label><input type="checkbox" value="${cat}" ${checked}> ${cat}</label>`;
  65. }).join('');
  66.  
  67. const limitInput = `<label>Check Limit: <input type="number" id="checkLimit" value="${checkLimit}" min="1"></label>`;
  68.  
  69. const settingsHTML = `
  70. <h2>Duplicate Filter Settings</h2>
  71. <h4>Select category or feed</h4>
  72. <div id="fdfs-categories">${categoryOptions}</div>
  73. ${limitInput}
  74. <br>
  75. <button id="fdfs-save">Save</button>
  76. <button id="fdfs-close">Close</button>
  77. `;
  78.  
  79. const settingsDiv = document.createElement('div');
  80. settingsDiv.id = "freshrss-duplicate-filter";
  81. settingsDiv.innerHTML = settingsHTML;
  82. document.body.appendChild(settingsDiv);
  83.  
  84. // Make settings panel draggable
  85. makeDraggable(settingsDiv);
  86.  
  87. // Save button event
  88. document.getElementById('fdfs-save').addEventListener('click', () => {
  89. const selectedCheckboxes = Array.from(document.querySelectorAll('input[type="checkbox"]:checked')).map(el => el.value);
  90. const newLimit = parseInt(document.getElementById('checkLimit').value, 10);
  91.  
  92. GM_setValue('selectedCategories', selectedCheckboxes);
  93. GM_setValue('checkLimit', newLimit);
  94.  
  95. showTooltip('Saved');
  96.  
  97. // Mark duplicates as read after saving
  98. markDuplicatesAsRead();
  99. });
  100.  
  101. // Close button event
  102. document.getElementById('fdfs-close').addEventListener('click', () => {
  103. document.body.removeChild(settingsDiv);
  104. });
  105. }
  106.  
  107. // Register settings screen
  108. GM_registerMenuCommand('Settings', showSettings);
  109.  
  110. // Function to display the tooltip
  111. function showTooltip(message) {
  112. // Create the tooltip element
  113. const tooltip = document.createElement('div');
  114. tooltip.textContent = message;
  115. tooltip.style.position = 'fixed';
  116. tooltip.style.top = '50%';
  117. tooltip.style.left = '50%';
  118. tooltip.style.transform = 'translate(-50%, -50%)';
  119. tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
  120. tooltip.style.color = 'white';
  121. tooltip.style.padding = '10px 20px';
  122. tooltip.style.borderRadius = '5px';
  123. tooltip.style.zIndex = '10000';
  124. tooltip.style.fontSize = '16px';
  125. tooltip.style.textAlign = 'center';
  126.  
  127. // Add the tooltip to the page
  128. document.body.appendChild(tooltip);
  129.  
  130. // Automatically remove the tooltip after 1 second
  131. setTimeout(() => {
  132. document.body.removeChild(tooltip);
  133. }, 1000);
  134. }
  135.  
  136. // Make element draggable
  137. function makeDraggable(elmnt) {
  138. const header = elmnt.querySelector('h2');
  139. let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
  140. header.onmousedown = dragMouseDown;
  141.  
  142. function dragMouseDown(e) {
  143. e = e || window.event;
  144. e.preventDefault();
  145. // Get the mouse cursor position at startup:
  146. pos3 = e.clientX;
  147. pos4 = e.clientY;
  148. document.onmouseup = closeDragElement;
  149. document.onmousemove = elementDrag;
  150. }
  151.  
  152. function elementDrag(e) {
  153. e = e || window.event;
  154. e.preventDefault();
  155. // Calculate the new cursor position:
  156. pos1 = pos3 - e.clientX;
  157. pos2 = pos4 - e.clientY;
  158. pos3 = e.clientX;
  159. pos4 = e.clientY;
  160. // Set the element's new position:
  161. elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
  162. elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
  163. }
  164.  
  165. function closeDragElement() {
  166. document.onmouseup = null;
  167. document.onmousemove = null;
  168. }
  169. }
  170.  
  171. // Mark as read and hide articles
  172. function markAsDuplicate(articleElement) {
  173. if (!articleElement) return;
  174.  
  175. // Check if mark_read function is available
  176. if (typeof mark_read === 'function') {
  177. mark_read(articleElement, true, true);
  178. } else {
  179. // Fallback: manually add 'read' class and trigger 'read' event
  180. articleElement.classList.add('read');
  181. const event = new Event('read');
  182. articleElement.dispatchEvent(event);
  183. }
  184.  
  185. // Hide the article
  186. articleElement.remove();
  187. }
  188.  
  189. // Check for duplicate articles and mark older ones as read
  190. function markDuplicatesAsRead() {
  191. const articles = Array.from(document.querySelectorAll('#stream > .flux'));
  192. const articleMap = new Map();
  193.  
  194. articles.slice(-checkLimit).forEach(article => {
  195. const titleElement = article.querySelector('a.item-element.title');
  196. if (!titleElement) return;
  197.  
  198. const title = titleElement.innerText;
  199. const url = titleElement.href;
  200. const timeElement = article.querySelector('.date > time');
  201. if (!timeElement) return;
  202.  
  203. const articleData = { element: article, timestamp: new Date(timeElement.datetime).getTime() };
  204.  
  205. // Duplicate check
  206. if (articleMap.has(title)) {
  207. const existingArticle = articleMap.get(title);
  208. if (existingArticle.url === url) {
  209. const older = existingArticle.timestamp < articleData.timestamp ? existingArticle : articleData;
  210.  
  211. // Mark older articles as duplicates
  212. markAsDuplicate(older.element);
  213. }
  214. } else {
  215. articleMap.set(title, { ...articleData, url });
  216. }
  217. });
  218. }
  219.  
  220.  
  221. // Get the current category
  222. function getCurrentCategory() {
  223. const categoryElement = document.querySelector('.category.active > ul > li.active > a > span.title');
  224. if (categoryElement) {
  225. return categoryElement.innerText;
  226. } else {
  227. const categoryElement_cat = document.querySelector('.category.active > a > span.title');
  228. if (categoryElement_cat) {
  229. return categoryElement_cat.innerText;
  230. } else {
  231. return null;
  232. }
  233. }
  234. }
  235.  
  236. // Setup MutationObserver
  237. function setupObserver() {
  238. const targetNode = document.querySelector('#stream');
  239. if (targetNode) {
  240. const observer = new MutationObserver(() => {
  241. const currentCategory = getCurrentCategory();
  242. if (currentCategory && selectedCategories.includes(currentCategory)) {
  243. markDuplicatesAsRead();
  244. }
  245. });
  246. observer.observe(targetNode, { childList: true, subtree: true });
  247.  
  248. // Initial run
  249. const currentCategory = getCurrentCategory();
  250. if (currentCategory && selectedCategories.includes(currentCategory)) {
  251. markDuplicatesAsRead();
  252. }
  253. } else {
  254. // Retry if #stream is not found
  255. setTimeout(setupObserver, 1000);
  256. }
  257. }
  258.  
  259. // Start setupObserver when the script starts
  260. setupObserver();
  261. })();