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