Anilist: Hide Unwanted Activity

Customize activity feeds by removing unwanted entries.

当前为 2023-09-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Anilist: Hide Unwanted Activity
  3. // @namespace https://github.com/SeyTi01/
  4. // @version 1.6
  5. // @description Customize activity feeds by removing unwanted entries.
  6. // @author SeyTi01
  7. // @match https://anilist.co/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. const config = {
  13. targetLoadCount: 2, // Number of activities to show per click on the "Load More" button
  14. remove: {
  15. uncommented: true, // Remove activities that have no comments
  16. unliked: false, // Remove activities that have no likes
  17. customStrings: [], // Remove activities with user-defined strings
  18. caseSensitive: false, // Whether string removal should be case-sensitive
  19. },
  20. runOn: {
  21. home: true, // Run the script on the home feed
  22. social: true, // Run the script on social feeds
  23. profile: false, // Run the script on user profile feeds
  24. },
  25. };
  26.  
  27. class ObserverManager {
  28.  
  29. constructor(activityHandler, uiHandler) {
  30. this.activity = activityHandler;
  31. this.ui = uiHandler;
  32. }
  33.  
  34. observeMutations(mutations) {
  35. if (this.isAllowedUrl()) {
  36. for (const mutation of mutations) {
  37. if (mutation.addedNodes.length > 0) {
  38. mutation.addedNodes.forEach(this.handleAddedNode.bind(this));
  39. }
  40. }
  41.  
  42. if (this.activity.currentLoadCount < config.targetLoadCount && this.ui.userPressedButton) {
  43. this.ui.clickLoadMore();
  44. } else {
  45. this.activity.resetState();
  46. this.ui.resetState();
  47. }
  48. }
  49. }
  50.  
  51. handleAddedNode(node) {
  52. if (node instanceof HTMLElement) {
  53. if (node.matches(SELECTORS.activity)) {
  54. this.activity.removeEntry(node);
  55.  
  56. } else if (node.matches(SELECTORS.button)) {
  57. this.ui.setLoadMoreButton(node);
  58. }
  59. }
  60. }
  61.  
  62. isAllowedUrl() {
  63. const currentUrl = window.location.href;
  64. return (
  65. (config.runOn.home && new RegExp(URLS.home.replace('*', '.*')).test(currentUrl)) ||
  66. (config.runOn.profile && new RegExp(URLS.profile.replace('*', '.*')).test(currentUrl)) ||
  67. (config.runOn.social && new RegExp(URLS.social.replace('*', '.*')).test(currentUrl))
  68. );
  69. }
  70.  
  71. initialize() {
  72. this.observer = new MutationObserver(this.observeMutations.bind(this));
  73. this.observer.observe(document.body, {childList: true, subtree: true});
  74. }
  75. }
  76.  
  77. class ActivityHandler {
  78.  
  79. constructor() {
  80. this.currentLoadCount = 0;
  81. }
  82.  
  83. removeEntry(node) {
  84. if (
  85. this.shouldRemoveUncommented(node) ||
  86. this.shouldRemoveUnliked(node) ||
  87. this.shouldRemoveByCustomStrings(node)
  88. ) {
  89. node.remove();
  90. } else {
  91. this.currentLoadCount++;
  92. }
  93. }
  94.  
  95. resetState() {
  96. this.currentLoadCount = 0;
  97. }
  98.  
  99. shouldRemoveUncommented(node) {
  100. if (config.remove.uncommented) {
  101. return !this.hasCountSpan(node.querySelector(SELECTORS.replies));
  102. }
  103. return false;
  104. }
  105.  
  106. shouldRemoveUnliked(node) {
  107. if (config.remove.unliked) {
  108. return !this.hasCountSpan(node.querySelector(SELECTORS.likes));
  109. }
  110. return false;
  111. }
  112.  
  113. shouldRemoveByCustomStrings(node) {
  114. return config.remove.customStrings.some((customString) => {
  115. return config.remove.caseSensitive
  116. ? node.textContent.includes(customString)
  117. : node.textContent.toLowerCase().includes(customString.toLowerCase());
  118. });
  119. }
  120.  
  121. hasCountSpan(node) {
  122. return node?.querySelector('span.count');
  123. }
  124. }
  125.  
  126. class UIHandler {
  127.  
  128. constructor() {
  129. this.userPressedButton = true;
  130. this.cancelButton = null;
  131. this.loadMoreButton = null;
  132. }
  133.  
  134. setLoadMoreButton(button) {
  135. this.loadMoreButton = button;
  136. this.loadMoreButton.addEventListener('click', () => {
  137. this.userPressedButton = true;
  138. this.simulateDomEvents();
  139. this.showCancelButton();
  140. });
  141. }
  142.  
  143. clickLoadMore() {
  144. if (this.loadMoreButton) {
  145. this.loadMoreButton.click();
  146. this.loadMoreButton = null;
  147. }
  148. }
  149.  
  150. resetState() {
  151. this.userPressedButton = false;
  152. if (this.cancelButton) {
  153. this.cancelButton.style.display = 'none';
  154. }
  155. }
  156.  
  157. showCancelButton() {
  158. if (!this.cancelButton) {
  159. this.createCancelButton();
  160. } else {
  161. this.cancelButton.style.display = 'block';
  162. }
  163. }
  164.  
  165. simulateDomEvents() {
  166. const domEvent = new Event('scroll', {bubbles: true});
  167. const intervalId = setInterval(() => {
  168. if (this.userPressedButton) {
  169. window.dispatchEvent(domEvent);
  170. } else {
  171. clearInterval(intervalId);
  172. }
  173. }, 100);
  174. }
  175.  
  176. createCancelButton() {
  177. const buttonStyles = `
  178. position: fixed;
  179. bottom: 10px;
  180. right: 10px;
  181. z-index: 9999;
  182. line-height: 1.3;
  183. background-color: rgb(var(--color-background-blue-dark));
  184. color: rgb(var(--color-text-bright));
  185. font: 1.6rem 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
  186. -webkit-font-smoothing: antialiased;
  187. box-sizing: border-box;
  188. `;
  189.  
  190. this.cancelButton = Object.assign(document.createElement('button'), {
  191. textContent: 'Cancel',
  192. className: 'cancel-button',
  193. style: `--button-color: rgb(var(--color-blue)); ${buttonStyles}`,
  194. onclick: () => {
  195. this.userPressedButton = false;
  196. this.cancelButton.style.display = 'none';
  197. },
  198. });
  199.  
  200. document.body.appendChild(this.cancelButton);
  201. }
  202. }
  203.  
  204. class ConfigValidator {
  205.  
  206. static validate(config) {
  207. const errors = [
  208. typeof config.remove.uncommented !== 'boolean' && 'remove.uncommented must be a boolean',
  209. typeof config.remove.unliked !== 'boolean' && 'remove.unliked must be a boolean',
  210. (!Number.isInteger(config.targetLoadCount) || config.targetLoadCount < 1) && 'targetLoadCount must be a positive non-zero integer',
  211. typeof config.runOn.home !== 'boolean' && 'runOn.home must be a boolean',
  212. typeof config.runOn.profile !== 'boolean' && 'runOn.profile must be a boolean',
  213. typeof config.runOn.social !== 'boolean' && 'runOn.social must be a boolean',
  214. !Array.isArray(config.remove.customStrings) && 'remove.customStrings must be an array',
  215. config.remove.customStrings.some((str) => typeof str !== 'string') && 'remove.customStrings must only contain strings',
  216. typeof config.remove.caseSensitive !== 'boolean' && 'remove.caseSensitive must be a boolean',
  217. ].filter(Boolean);
  218.  
  219. if (errors.length > 0) {
  220. console.error('Script configuration errors:');
  221. errors.forEach((error) => console.error(error));
  222. return false;
  223. }
  224.  
  225. return true;
  226. }
  227. }
  228.  
  229. const SELECTORS = {
  230. button: 'div.load-more',
  231. activity: 'div.activity-entry',
  232. replies: 'div.action.replies',
  233. likes: 'div.action.likes',
  234. };
  235.  
  236. const URLS = {
  237. home: 'https://anilist.co/home',
  238. profile: 'https://anilist.co/user/*/',
  239. social: 'https://anilist.co/*/social',
  240. };
  241.  
  242. (function() {
  243. 'use strict';
  244. if (!ConfigValidator.validate(config)) {
  245. console.error('Script disabled due to configuration errors.');
  246. } else {
  247. const activityHandler = new ActivityHandler();
  248. const uiHandler = new UIHandler();
  249. new ObserverManager(activityHandler, uiHandler).initialize();
  250. }
  251. })();