您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Control the content displayed in your activity feeds
// ==UserScript== // @name Anilist: Activity-Feed Filter // @namespace https://github.com/SeyTi01/ // @version 1.8.5 // @description Control the content displayed in your activity feeds // @author SeyTi01 // @match https://anilist.co/* // @grant none // @license MIT // ==/UserScript== const config = { remove: { images: false, // Remove activities with images gifs: false, // Remove activities with gifs videos: false, // Remove activities with videos text: false, // Remove activities with only text uncommented: false, // Remove activities without comments unliked: false, // Remove activities without likes containsStrings: [], // Remove activities containing user-defined strings }, options: { targetLoadCount: 2, // Minimum number of activities to display per "Load More" button click caseSensitive: false, // Use case-sensitive matching for string-based removal reverseConditions: false, // Display only posts that meet the specified removal conditions linkedConditions: [], // Groups of conditions to be evaluated together }, runOn: { home: true, // Run the script on the home feed social: true, // Run the script on the 'Recent Activity' of anime/manga entries profile: false, // Run the script on user profile feeds guestHome: false, // Run the script on the home feed for non-user visitors }, }; class MainApp { constructor(activityHandler, uiHandler, config) { this.ac = activityHandler; this.ui = uiHandler; this.config = config; this.URLS = { home: 'https://anilist.co/home', social: 'https://anilist.co/*/social', profile: 'https://anilist.co/user/*/', guestHome: 'https://anilist.co/social', }; } initializeObserver() { this.observer = new MutationObserver(this._observeMutations.bind(this)); this.observer.observe(document.body, { childList: true, subtree: true }); } _observeMutations(mutations) { if (this._isUrlAllowed()) { mutations.forEach(mutation => mutation.addedNodes.forEach(node => this._handleAddedNode(node))); this._processLoadOrReset(); } } _handleAddedNode(node) { if (!(node instanceof HTMLElement)) { return; } if (node.matches(SELECTORS.DIV.ACTIVITY)) { this.ac.processActivityNode(node); } else if (node.matches(SELECTORS.DIV.BUTTON)) { this.ui.bindLoadMoreButton(node); } else if (node.matches(SELECTORS.DIV.MARKDOWN)) { const entry = node.closest(SELECTORS.DIV.ACTIVITY); if (entry) this.ac.processActivityNode(entry); } } _processLoadOrReset() { if (this.ac.currentLoadCount < this.config.options.targetLoadCount && this.ui.userPressed) { this.ui.triggerLoadMore(); } else { this.ac._resetLoadCount(); this.ui.resetUIState(); } } _isUrlAllowed() { const allowedPatterns = Object.keys(this.URLS).filter(pattern => this.config.runOn[pattern]); return allowedPatterns.some(pattern => { const regex = new RegExp(this.URLS[pattern].replace('*', '.*')); return regex.test(window.location.href); }); } } class ActivityHandler { constructor(config) { this.currentLoadCount = 0; this.config = config; this.LINKED = { TRUE: 1, FALSE: 0, NONE: -1 }; const wrap = method => (node, reverse) => { const res = method.call(this, node); return reverse ? !res : res; }; const handlers = { uncommented: this._evaluateUncommentedRemoval, unliked: this._evaluateUnlikedRemoval, text: this._evaluateTextRemoval, images: this._evaluateImageRemoval, gifs: this._evaluateGifRemoval, videos: this._evaluateVideoRemoval, containsStrings: this._evaluateStringRemoval }; this.CONDITIONS_MAP = new Map( Object.entries(handlers).map( ([name, method]) => [name, wrap(method)] ) ); } processActivityNode(node) { const { options: { reverseConditions, linkedConditions } } = this.config; this.linkedConditionsFlat = linkedConditions.flat(); const linkedResult = this._evaluateLinkedConditions(node); const shouldRemove = reverseConditions ? this._evaluateReverseConditions(node, linkedResult) : this._evaluateNormalConditions(node, linkedResult); shouldRemove ? node.remove() : this.currentLoadCount++; } _evaluateLinkedConditions(node) { const { options: { linkedConditions } } = this.config; if (this.linkedConditionsFlat.length === 0) { return this.LINKED.NONE; } const lists = this._extractLinkedConditions(linkedConditions); const results = lists.map(list => this._evaluateConditionList(node, list)); const hasTrue = results.some(Boolean); const hasFalse = results.some(r => !r); return hasTrue && (!this.config.options.reverseConditions || !hasFalse) ? this.LINKED.TRUE : this.LINKED.FALSE; } _evaluateReverseConditions(node, linkedResult) { const { options: { reverseConditions } } = this.config; const results = this._getActiveConditionFunctions().map(fn => fn(node, reverseConditions)); return linkedResult !== this.LINKED.FALSE && !results.includes(false) && (linkedResult === this.LINKED.TRUE || results.includes(true)); } _evaluateNormalConditions(node, linkedResult) { const { options: { reverseConditions } } = this.config; const anyMatch = this._getActiveConditionFunctions().some(fn => fn(node, reverseConditions)); return linkedResult === this.LINKED.TRUE || anyMatch; } _getActiveConditionFunctions() { const { remove } = this.config; return [...this.CONDITIONS_MAP] .filter(([name]) => { if (this.linkedConditionsFlat.includes(name)) return false; const cfg = remove[name]; return cfg === true || (Array.isArray(cfg) && cfg.flat().length > 0); }) .map(([, fn]) => fn); } _evaluateConditionList(node, list) { const { options: { reverseConditions } } = this.config; return reverseConditions ? list.some(cond => this.CONDITIONS_MAP.get(cond)(node, reverseConditions)) : list.every(cond => this.CONDITIONS_MAP.get(cond)(node, reverseConditions)); } _extractLinkedConditions(linkedConditions) { const isNested = linkedConditions.some(Array.isArray); return isNested ? linkedConditions.map(c => Array.isArray(c) ? c : [c]) : [linkedConditions]; } _evaluateStringRemoval(node) { const { remove: { containsStrings }, options: { caseSensitive } } = this.config; const matches = substr => { const text = node.textContent; return caseSensitive ? text.includes(substr) : text.toLowerCase().includes(substr.toLowerCase()); }; return containsStrings.some(group => Array.isArray(group) ? group.every(matches) : matches(group) ); } _evaluateTextRemoval(node) { const hasTextClass = node.classList.contains(SELECTORS.ACTIVITY.TEXT) || node.classList.contains(SELECTORS.ACTIVITY.MESSAGE); return hasTextClass && !( this._evaluateImageRemoval(node) || this._evaluateGifRemoval(node) || this._evaluateVideoRemoval(node) ); } _evaluateVideoRemoval(node) { return node.querySelector(SELECTORS.CLASS.VIDEO) || node.querySelector(SELECTORS.SPAN.YOUTUBE); } _evaluateImageRemoval(node) { const img = node.querySelector(SELECTORS.CLASS.IMAGE); return img && !img.src.includes('.gif'); } _evaluateGifRemoval(node) { const img = node.querySelector(SELECTORS.CLASS.IMAGE); return img && img.src.includes('.gif'); } _evaluateUncommentedRemoval(node) { const replies = node.querySelector(SELECTORS.DIV.REPLIES); return !replies || !replies.querySelector(SELECTORS.SPAN.COUNT); } _evaluateUnlikedRemoval(node) { const likes = node.querySelector(SELECTORS.DIV.LIKES); return !likes || !likes.querySelector(SELECTORS.SPAN.COUNT); } _resetLoadCount() { this.currentLoadCount = 0; } } class UIHandler { constructor() { this.userPressed = true; this.loadMoreButton = null; this.cancelButton = null; } bindLoadMoreButton(button) { this.loadMoreButton = button; button.addEventListener('click', () => { this.userPressed = true; this._startScrollTrigger(); this._showCancelButton(); }); } triggerLoadMore() { this.loadMoreButton?.click(); } resetUIState() { this.userPressed = false; this._hideCancelButton(); } displayErrorMessage(message) { if (!this.errorContainer) { const style = `position: fixed;` + `bottom: 10px;` + `right: 10px;` + `z-index: 10000;` + `background-color: rgba(255,0,0,0.85);` + `color: #fff;` + `padding: 12px 20px;` + `border-radius: 4px;` + `font: 1.4rem Roboto, sans-serif;` + `box-shadow: 0 2px 6px rgba(0,0,0,0.3);`; this.errorContainer = Object.assign( document.createElement('div'), { textContent: message, className: 'config-error-message', } ); this.errorContainer.setAttribute('style', style); document.body.appendChild(this.errorContainer); } else { this.errorContainer.textContent = message; this.errorContainer.style.display = 'block'; } setTimeout(() => { if (this.errorContainer) { this.errorContainer.style.display = 'none'; } }, 5000); } _createCancelButton() { if (this.cancelButton) { this.cancelButton.style.display = 'block'; return; } const style = `position: fixed;` + `bottom: 10px;` + `right: 10px;` + `z-index: 9999;` + `line-height: 1.3;` + `background-color: rgb(var(--color-background-blue-dark));` + `color: rgb(var(--color-text-bright));` + `font: 1.6rem Roboto, sans-serif;` + `box-sizing: border-box;`; this.cancelButton = document.createElement('button'); this.cancelButton.textContent = 'Cancel'; this.cancelButton.className = 'cancel-button'; this.cancelButton.setAttribute('style', style); this.cancelButton.addEventListener('click', () => { this.userPressed = false; this.cancelButton.style.display = 'none'; }); document.body.appendChild(this.cancelButton); } _showCancelButton() { if (this.cancelButton) { this.cancelButton.style.display = 'block'; } else { this._createCancelButton(); } } _hideCancelButton() { if (this.cancelButton) { this.cancelButton.style.display = 'none'; } } _startScrollTrigger() { const event = new Event('scroll', { bubbles: true }); const interval = setInterval(() => { this.userPressed ? window.dispatchEvent(event) : clearInterval(interval); }, 100); } } class ConfigValidator { constructor(config) { this.config = config; this.errors = []; } validateConfig() { this._validatePositiveInteger('options.targetLoadCount'); this._validateStringArray('remove.containsStrings'); this._validateStringArray('options.linkedConditions'); this._validateLinkedConditions(); this._validateBooleanSettings([ 'remove.uncommented', 'remove.unliked', 'remove.text', 'remove.images', 'remove.gifs', 'remove.videos', 'options.caseSensitive', 'options.reverseConditions', 'runOn.home', 'runOn.social', 'runOn.profile', 'runOn.guestHome' ]); if (this.errors.length) { throw new Error(`Anilist Activity Feed Filter: Script disabled due to configuration errors: ${this.errors.join(', ')}`); } } _validateLinkedConditions() { const linked = this._flattenArray(this._getConfigValue('options.linkedConditions')); const allowed = ['uncommented', 'unliked', 'text', 'images', 'gifs', 'videos', 'containsStrings']; if (linked.some(cond => !allowed.includes(cond))) { this.errors.push(`options.linkedConditions should only contain: ${allowed.join(', ')}`); } } _validateBooleanSettings(paths) { paths.forEach(path => { if (typeof this._getConfigValue(path) !== 'boolean') { this.errors.push(`${path} should be a boolean`); } }); } _validateStringArray(path) { const value = this._getConfigValue(path); if (!Array.isArray(value)) { this.errors.push(`${path} should be an array`); } else if (!this._flattenArray(value).every(item => typeof item === 'string')) { this.errors.push(`${path} should only contain strings`); } } _validatePositiveInteger(path) { const value = this._getConfigValue(path); if (!Number.isInteger(value) || value <= 0) { this.errors.push(`${path} should be a positive non-zero integer`); } } _getConfigValue(path) { return path.split('.').reduce((obj, key) => obj[key], this.config); } _flattenArray(arr) { return arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? this._flattenArray(val) : val), []); } } const SELECTORS = { DIV: { BUTTON: 'div.load-more', ACTIVITY: 'div.activity-entry', REPLIES: 'div.action.replies', LIKES: 'div.action.likes', MARKDOWN: 'div.markdown' }, SPAN: { COUNT: 'span.count', YOUTUBE: 'span.youtube', }, ACTIVITY: { TEXT: 'activity-text', MESSAGE: 'activity-message', }, CLASS: { IMAGE: 'img', VIDEO: 'video', }, }; function initializeApp() { const uiHandler = new UIHandler(); try { new ConfigValidator(config).validateConfig(); } catch (error) { uiHandler.displayErrorMessage(error.message); return; } const activityHandler = new ActivityHandler(config); const mainApp = new MainApp(activityHandler, uiHandler, config); mainApp.initializeObserver(); } initializeApp();