Anilist: Hide Unwanted Activity

Customize activity feeds by removing unwanted entries.

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

  1. // ==UserScript==
  2. // @name Anilist: Hide Unwanted Activity
  3. // @namespace https://github.com/SeyTi01/
  4. // @version 1.7
  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, // Minimum 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. images: false, // Remove activities containing images
  18. videos: false, // Remove activities containing videos
  19. customStrings: [], // Remove activities with user-defined strings
  20. caseSensitive: false, // Whether string removal should be case-sensitive
  21. },
  22. runOn: {
  23. home: true, // Run the script on the home feed
  24. social: true, // Run the script on social feeds
  25. profile: false, // Run the script on user profile feeds
  26. },
  27. linkedConditions: [
  28. [] // Groups of conditions to be checked together (linked conditions are always considered 'true')
  29. ],
  30. };
  31.  
  32. class MainApp {
  33.  
  34. constructor(activityHandler, uiHandler) {
  35. this.ac = activityHandler;
  36. this.ui = uiHandler;
  37. }
  38.  
  39. observeMutations(mutations) {
  40. if (this.isAllowedUrl()) {
  41. for (const mutation of mutations) {
  42. if (mutation.addedNodes.length > 0) {
  43. mutation.addedNodes.forEach(node => this.handleAddedNode(node));
  44. }
  45. }
  46.  
  47. this.loadMoreOrReset();
  48. }
  49. }
  50.  
  51. handleAddedNode(node) {
  52. if (node instanceof HTMLElement) {
  53. if (node.matches(SELECTORS.div.activity)) {
  54. this.ac.removeEntry(node);
  55. } else if (node.matches(SELECTORS.div.button)) {
  56. this.ui.setLoadMore(node);
  57. }
  58. }
  59. }
  60.  
  61. loadMoreOrReset() {
  62. if (this.ac.currentLoadCount < config.targetLoadCount && this.ui.userPressed) {
  63. this.ui.clickLoadMore();
  64. } else {
  65. this.ac.resetState();
  66. this.ui.resetState();
  67. }
  68. }
  69.  
  70. isAllowedUrl() {
  71. const currentUrl = window.location.href;
  72. const allowedPatterns = Object.keys(this.URLS).filter(pattern => config.runOn[pattern]);
  73.  
  74. return allowedPatterns.some(pattern => {
  75. const regex = new RegExp(this.URLS[pattern].replace('*', '.*'));
  76. return regex.test(currentUrl);
  77. });
  78. }
  79.  
  80. initializeObserver() {
  81. this.observer = new MutationObserver(this.observeMutations.bind(this));
  82. this.observer.observe(document.body, {childList: true, subtree: true});
  83. }
  84.  
  85. URLS = {
  86. home: 'https://anilist.co/home',
  87. profile: 'https://anilist.co/user/*/',
  88. social: 'https://anilist.co/*/social',
  89. };
  90. }
  91.  
  92. class ActivityHandler {
  93.  
  94. constructor() {
  95. this.currentLoadCount = 0;
  96. }
  97.  
  98. conditionsMap = new Map([
  99. ['uncommented', function(node) { return this.shouldRemoveUncommented(node); }.bind(this)],
  100. ['unliked', function(node) { return this.shouldRemoveUnliked(node); }.bind(this)],
  101. ['images', function(node) { return this.shouldRemoveImage(node); }.bind(this)],
  102. ['videos', function(node) { return this.shouldRemoveVideo(node); }.bind(this)],
  103. ['customStrings', function(node) { return this.shouldRemoveByCustomStrings(node); }.bind(this)]
  104. ]);
  105.  
  106. removeEntry(node) {
  107. if (this.shouldRemoveNode(node)) {
  108. node.remove();
  109. } else {
  110. this.currentLoadCount++;
  111. }
  112. }
  113.  
  114. resetState() {
  115. this.currentLoadCount = 0;
  116. }
  117.  
  118. shouldRemoveNode(node) {
  119. const checkCondition = (conditionName, predicate) => {
  120. return (
  121. config.remove[conditionName] &&
  122. predicate(node) &&
  123. !config.linkedConditions.some(innerArray => innerArray.includes(conditionName))
  124. );
  125. };
  126.  
  127. if (this.shouldRemoveByLinkedConditions(node)) {
  128. return true;
  129. }
  130.  
  131. const conditions = Array.from(this.conditionsMap.entries());
  132. return conditions.some(([name, predicate]) => checkCondition(name, predicate));
  133. }
  134.  
  135. shouldRemoveByLinkedConditions(node) {
  136. return !config.linkedConditions.every(link => link.length === 0) &&
  137. config.linkedConditions.some(link => link.every(condition => this.conditionsMap.get(condition)(node)));
  138. }
  139.  
  140. shouldRemoveUncommented(node) {
  141. return !this.hasElement(SELECTORS.span.count, node.querySelector(SELECTORS.div.replies));
  142. }
  143.  
  144. shouldRemoveUnliked(node) {
  145. return !this.hasElement(SELECTORS.span.count, node.querySelector(SELECTORS.div.likes));
  146. }
  147.  
  148. shouldRemoveImage(node) {
  149. return this.hasElement(SELECTORS.class.image, node);
  150. }
  151.  
  152. shouldRemoveVideo(node) {
  153. return this.hasElement(SELECTORS.class.video, node);
  154. }
  155.  
  156. shouldRemoveByCustomStrings(node) {
  157. return config.remove.customStrings.some((customString) =>
  158. (config.remove.caseSensitive ?
  159. node.textContent.includes(customString) :
  160. node.textContent.toLowerCase().includes(customString.toLowerCase()))
  161. );
  162. }
  163.  
  164. hasElement(selector, node) {
  165. return node?.querySelector(selector);
  166. }
  167. }
  168.  
  169. class UIHandler {
  170.  
  171. constructor() {
  172. this.userPressed = true;
  173. this.cancel = null;
  174. this.loadMore = null;
  175. }
  176.  
  177. setLoadMore(button) {
  178. this.loadMore = button;
  179. this.loadMore.addEventListener('click', () => {
  180. this.userPressed = true;
  181. this.simulateDomEvents();
  182. this.showCancel();
  183. });
  184. }
  185.  
  186. clickLoadMore() {
  187. if (this.loadMore) {
  188. this.loadMore.click();
  189. this.loadMore = null;
  190. }
  191. }
  192.  
  193. resetState() {
  194. this.userPressed = false;
  195. this.hideCancel();
  196. }
  197.  
  198. showCancel() {
  199. if (!this.cancel) {
  200. this.createCancel();
  201. } else {
  202. this.cancel.style.display = 'block';
  203. }
  204. }
  205.  
  206. hideCancel() {
  207. if (this.cancel) {
  208. this.cancel.style.display = 'none';
  209. }
  210. }
  211.  
  212. simulateDomEvents() {
  213. const domEvent = new Event('scroll', {bubbles: true});
  214. const intervalId = setInterval(() => {
  215. if (this.userPressed) {
  216. window.dispatchEvent(domEvent);
  217. } else {
  218. clearInterval(intervalId);
  219. }
  220. }, 100);
  221. }
  222.  
  223. createCancel() {
  224. const BUTTON_STYLE = `
  225. position: fixed;
  226. bottom: 10px;
  227. right: 10px;
  228. z-index: 9999;
  229. line-height: 1.3;
  230. background-color: rgb(var(--color-background-blue-dark));
  231. color: rgb(var(--color-text-bright));
  232. font: 1.6rem 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
  233. -webkit-font-smoothing: antialiased;
  234. box-sizing: border-box;
  235. --button-color: rgb(var(--color-blue));
  236. `;
  237.  
  238. this.cancel = Object.assign(document.createElement('button'), {
  239. textContent: 'Cancel',
  240. className: 'cancel-button',
  241. style: BUTTON_STYLE,
  242. onclick: () => {
  243. this.userPressed = false;
  244. this.cancel.style.display = 'none';
  245. },
  246. });
  247.  
  248. document.body.appendChild(this.cancel);
  249. }
  250. }
  251.  
  252. class ConfigValidator {
  253.  
  254. static validate(config) {
  255. const errors = [
  256. typeof config.remove.uncommented !== 'boolean' && 'remove.uncommented must be a boolean',
  257. typeof config.remove.unliked !== 'boolean' && 'remove.unliked must be a boolean',
  258. typeof config.remove.images !== 'boolean' && 'remove.images must be a boolean',
  259. typeof config.remove.videos !== 'boolean' && 'remove.videos must be a boolean',
  260. (!Number.isInteger(config.targetLoadCount) || config.targetLoadCount < 1) && 'targetLoadCount must be a positive non-zero integer',
  261. typeof config.runOn.home !== 'boolean' && 'runOn.home must be a boolean',
  262. typeof config.runOn.profile !== 'boolean' && 'runOn.profile must be a boolean',
  263. typeof config.runOn.social !== 'boolean' && 'runOn.social must be a boolean',
  264. !Array.isArray(config.remove.customStrings) && 'remove.customStrings must be an array',
  265. config.remove.customStrings.some((str) => typeof str !== 'string') && 'remove.customStrings must only contain strings',
  266. typeof config.remove.caseSensitive !== 'boolean' && 'remove.caseSensitive must be a boolean',
  267. !Array.isArray(config.linkedConditions) && 'linkedConditions must be an array',
  268. config.linkedConditions.some((conditionGroup) => {
  269. if (!Array.isArray(conditionGroup)) return true;
  270. return conditionGroup.some((condition) => {
  271. if (typeof condition !== 'string' && !Array.isArray(condition)) return true;
  272. if (Array.isArray(condition)) {
  273. return condition.some((item) => !['uncommented', 'unliked', 'images', 'videos', 'customStrings'].includes(item));
  274. }
  275. return !['uncommented', 'unliked', 'images', 'videos', 'customStrings'].includes(condition);
  276. });
  277. }) && 'linkedConditions must only contain arrays with valid strings',
  278. ].filter(Boolean);
  279.  
  280. if (errors.length > 0) {
  281. console.error('Script configuration errors:');
  282. errors.forEach((error) => console.error(error));
  283. return false;
  284. }
  285.  
  286. return true;
  287. }
  288. }
  289.  
  290. const SELECTORS = {
  291. div: {
  292. button: 'div.load-more',
  293. activity: 'div.activity-entry',
  294. replies: 'div.action.replies',
  295. likes: 'div.action.likes',
  296. },
  297. span: {
  298. count: 'span.count',
  299. },
  300. class: {
  301. image: 'img',
  302. video: 'video',
  303. }
  304. };
  305.  
  306. (function() {
  307. 'use strict';
  308.  
  309. if (!ConfigValidator.validate(config)) {
  310. console.error('Script disabled due to configuration errors.');
  311. return;
  312. }
  313.  
  314. const activityHandler = new ActivityHandler();
  315. const uiHandler = new UIHandler();
  316. const mainApp = new MainApp(activityHandler, uiHandler);
  317.  
  318. mainApp.initializeObserver();
  319. })();