FreshRSS Duplicate Filter

Hide older articles that have both the same title and URL within a category

目前为 2024-09-21 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name FreshRSS Duplicate Filter
  3. // @namespace https://github.com/hiroki-miya
  4. // @version 1.0.0
  5. // @description Hide older articles that have both the same title and URL within a category
  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. }
  40. #freshrss-duplicate-filter > h2 {
  41. box-shadow: inset 0 0 0 0.5px black;
  42. padding: 5px 10px;
  43. text-align: center;
  44. }
  45. #freshrss-duplicate-filter > h4 {
  46. margin-top: 0;
  47. }
  48. `);
  49.  
  50. // Normalize category name: remove spaces and everything after newlines
  51. function normalizeCategoryName(categoryName) {
  52. return categoryName.replace(/[ \r\n].*/g, '');
  53. }
  54.  
  55. // Settings screen
  56. function showSettings() {
  57. const categories = Array.from(document.querySelectorAll('.tree-folder.category')).map(cat => normalizeCategoryName(cat.innerText));
  58. const selected = selectedCategories || [];
  59.  
  60. let categoryOptions = categories.map(cat => {
  61. const checked = selected.includes(cat) ? 'checked' : '';
  62. return `<label><input type="checkbox" value="${cat}" ${checked}> ${cat}</label>`;
  63. }).join('');
  64.  
  65. const limitInput = `<label>Check Limit: <input type="number" id="checkLimit" value="${checkLimit}" min="1"></label>`;
  66.  
  67. const settingsHTML = `
  68. <h2>Duplicate Filter Settings</h2>
  69. <h4>Select Categories</h4>
  70. ${categoryOptions}
  71. ${limitInput}
  72. <br>
  73. <button id="fdfs-save">Save</button>
  74. <button id="fdfs-close">Close</button>
  75. `;
  76.  
  77. const settingsDiv = document.createElement('div');
  78. settingsDiv.id = "freshrss-duplicate-filter";
  79. settingsDiv.innerHTML = settingsHTML;
  80. document.body.appendChild(settingsDiv);
  81.  
  82. // Function to display the tooltip
  83. function showTooltip(message) {
  84. // Create the tooltip element
  85. const tooltip = document.createElement('div');
  86. tooltip.textContent = message;
  87. tooltip.style.position = 'fixed';
  88. tooltip.style.top = '50%';
  89. tooltip.style.left = '50%';
  90. tooltip.style.transform = 'translate(-50%, -50%)';
  91. tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
  92. tooltip.style.color = 'white';
  93. tooltip.style.padding = '10px 20px';
  94. tooltip.style.borderRadius = '5px';
  95. tooltip.style.zIndex = '10000';
  96. tooltip.style.fontSize = '16px';
  97. tooltip.style.textAlign = 'center';
  98.  
  99. // Add the tooltip to the page
  100. document.body.appendChild(tooltip);
  101.  
  102. // Automatically remove the tooltip after 1 second
  103. setTimeout(() => {
  104. document.body.removeChild(tooltip);
  105. }, 1000);
  106. }
  107.  
  108. // Save button event
  109. document.getElementById('fdfs-save').addEventListener('click', () => {
  110. const selectedCheckboxes = Array.from(document.querySelectorAll('input[type="checkbox"]:checked')).map(el => el.value);
  111. const newLimit = parseInt(document.getElementById('checkLimit').value, 10);
  112.  
  113. GM_setValue('selectedCategories', selectedCheckboxes);
  114. GM_setValue('checkLimit', newLimit);
  115.  
  116. // Show tooltip instead of alert
  117. showTooltip('Saved');
  118.  
  119. // Mark duplicates as read after saving
  120. markDuplicatesAsRead();
  121. });
  122.  
  123. // Close button event
  124. document.getElementById('fdfs-close').addEventListener('click', () => {
  125. document.body.removeChild(settingsDiv);
  126. });
  127. }
  128.  
  129. // Register settings screen
  130. GM_registerMenuCommand('Settings', showSettings);
  131.  
  132. // Function to add "duplicate" class to articles
  133. function markAsDuplicate(articleElement) {
  134. if (!articleElement) return;
  135. articleElement.classList.add('duplicate');
  136. articleElement.style.display = 'none';
  137. }
  138.  
  139. // Check for duplicate articles and mark older ones as read
  140. function markDuplicatesAsRead() {
  141. const articles = Array.from(document.querySelectorAll('#stream > .flux:not(:has(.duplicate))'));
  142. const articleMap = new Map();
  143.  
  144. articles.slice(-checkLimit).forEach(article => {
  145. const titleElement = article.querySelector('a.item-element.title');
  146. if (!titleElement) return;
  147.  
  148. const title = titleElement.innerText;
  149. const url = titleElement.href;
  150. const timeElement = article.querySelector('.date > time');
  151. if (!timeElement) return;
  152.  
  153. const articleData = { element: article, timestamp: new Date(timeElement.datetime).getTime() };
  154.  
  155. // Duplicate check
  156. if (articleMap.has(title)) {
  157. const existingArticle = articleMap.get(title);
  158. if (existingArticle.url === url) {
  159. const older = existingArticle.timestamp <= articleData.timestamp ? existingArticle : articleData;
  160. // Mark older articles as duplicates
  161. markAsDuplicate(older.element);
  162. }
  163. } else {
  164. articleMap.set(title, { ...articleData, url });
  165. }
  166. });
  167. }
  168.  
  169. // Get the current category
  170. function getCurrentCategory() {
  171. const categoryElement = document.querySelector('.category.active > a > span.title');
  172. return categoryElement ? normalizeCategoryName(categoryElement.innerText) : null;
  173. }
  174.  
  175. // Setup MutationObserver
  176. function setupObserver() {
  177. const targetNode = document.querySelector('#stream');
  178. if (targetNode) {
  179. const observer = new MutationObserver(() => {
  180. const currentCategory = getCurrentCategory();
  181. if (currentCategory && selectedCategories.includes(currentCategory)) {
  182. markDuplicatesAsRead();
  183. }
  184. });
  185. observer.observe(targetNode, { childList: true, subtree: true });
  186.  
  187. // Initial run
  188. const currentCategory = getCurrentCategory();
  189. if (currentCategory && selectedCategories.includes(currentCategory)) {
  190. markDuplicatesAsRead();
  191. }
  192. } else {
  193. // Retry if #stream is not found
  194. setTimeout(setupObserver, 1000);
  195. }
  196. }
  197.  
  198. // Start setupObserver when the script starts
  199. setupObserver();
  200. })();