- // ==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();