// ==UserScript==
// @name Anilist: Hide Unwanted Activity
// @namespace https://github.com/SeyTi01/
// @version 1.7
// @description Customize activity feeds by removing unwanted entries.
// @author SeyTi01
// @match https://anilist.co/*
// @grant none
// @license MIT
// ==/UserScript==
const config = {
targetLoadCount: 2, // Minimum number of activities to show per click on the "Load More" button
remove: {
uncommented: true, // Remove activities that have no comments
unliked: false, // Remove activities that have no likes
images: false, // Remove activities containing images
videos: false, // Remove activities containing videos
customStrings: [], // Remove activities with user-defined strings
caseSensitive: false, // Whether string removal should be case-sensitive
},
runOn: {
home: true, // Run the script on the home feed
social: true, // Run the script on social feeds
profile: false, // Run the script on user profile feeds
},
linkedConditions: [
[] // Groups of conditions to be checked together (linked conditions are always considered 'true')
],
};
class MainApp {
constructor(activityHandler, uiHandler) {
this.ac = activityHandler;
this.ui = uiHandler;
}
observeMutations(mutations) {
if (this.isAllowedUrl()) {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => this.handleAddedNode(node));
}
}
this.loadMoreOrReset();
}
}
handleAddedNode(node) {
if (node instanceof HTMLElement) {
if (node.matches(SELECTORS.div.activity)) {
this.ac.removeEntry(node);
} else if (node.matches(SELECTORS.div.button)) {
this.ui.setLoadMore(node);
}
}
}
loadMoreOrReset() {
if (this.ac.currentLoadCount < config.targetLoadCount && this.ui.userPressed) {
this.ui.clickLoadMore();
} else {
this.ac.resetState();
this.ui.resetState();
}
}
isAllowedUrl() {
const currentUrl = window.location.href;
const allowedPatterns = Object.keys(this.URLS).filter(pattern => config.runOn[pattern]);
return allowedPatterns.some(pattern => {
const regex = new RegExp(this.URLS[pattern].replace('*', '.*'));
return regex.test(currentUrl);
});
}
initializeObserver() {
this.observer = new MutationObserver(this.observeMutations.bind(this));
this.observer.observe(document.body, {childList: true, subtree: true});
}
URLS = {
home: 'https://anilist.co/home',
profile: 'https://anilist.co/user/*/',
social: 'https://anilist.co/*/social',
};
}
class ActivityHandler {
constructor() {
this.currentLoadCount = 0;
}
conditionsMap = new Map([
['uncommented', function(node) { return this.shouldRemoveUncommented(node); }.bind(this)],
['unliked', function(node) { return this.shouldRemoveUnliked(node); }.bind(this)],
['images', function(node) { return this.shouldRemoveImage(node); }.bind(this)],
['videos', function(node) { return this.shouldRemoveVideo(node); }.bind(this)],
['customStrings', function(node) { return this.shouldRemoveByCustomStrings(node); }.bind(this)]
]);
removeEntry(node) {
if (this.shouldRemoveNode(node)) {
node.remove();
} else {
this.currentLoadCount++;
}
}
resetState() {
this.currentLoadCount = 0;
}
shouldRemoveNode(node) {
const checkCondition = (conditionName, predicate) => {
return (
config.remove[conditionName] &&
predicate(node) &&
!config.linkedConditions.some(innerArray => innerArray.includes(conditionName))
);
};
if (this.shouldRemoveByLinkedConditions(node)) {
return true;
}
const conditions = Array.from(this.conditionsMap.entries());
return conditions.some(([name, predicate]) => checkCondition(name, predicate));
}
shouldRemoveByLinkedConditions(node) {
return !config.linkedConditions.every(link => link.length === 0) &&
config.linkedConditions.some(link => link.every(condition => this.conditionsMap.get(condition)(node)));
}
shouldRemoveUncommented(node) {
return !this.hasElement(SELECTORS.span.count, node.querySelector(SELECTORS.div.replies));
}
shouldRemoveUnliked(node) {
return !this.hasElement(SELECTORS.span.count, node.querySelector(SELECTORS.div.likes));
}
shouldRemoveImage(node) {
return this.hasElement(SELECTORS.class.image, node);
}
shouldRemoveVideo(node) {
return this.hasElement(SELECTORS.class.video, node);
}
shouldRemoveByCustomStrings(node) {
return config.remove.customStrings.some((customString) =>
(config.remove.caseSensitive ?
node.textContent.includes(customString) :
node.textContent.toLowerCase().includes(customString.toLowerCase()))
);
}
hasElement(selector, node) {
return node?.querySelector(selector);
}
}
class UIHandler {
constructor() {
this.userPressed = true;
this.cancel = null;
this.loadMore = null;
}
setLoadMore(button) {
this.loadMore = button;
this.loadMore.addEventListener('click', () => {
this.userPressed = true;
this.simulateDomEvents();
this.showCancel();
});
}
clickLoadMore() {
if (this.loadMore) {
this.loadMore.click();
this.loadMore = null;
}
}
resetState() {
this.userPressed = false;
this.hideCancel();
}
showCancel() {
if (!this.cancel) {
this.createCancel();
} else {
this.cancel.style.display = 'block';
}
}
hideCancel() {
if (this.cancel) {
this.cancel.style.display = 'none';
}
}
simulateDomEvents() {
const domEvent = new Event('scroll', {bubbles: true});
const intervalId = setInterval(() => {
if (this.userPressed) {
window.dispatchEvent(domEvent);
} else {
clearInterval(intervalId);
}
}, 100);
}
createCancel() {
const BUTTON_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', -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
--button-color: rgb(var(--color-blue));
`;
this.cancel = Object.assign(document.createElement('button'), {
textContent: 'Cancel',
className: 'cancel-button',
style: BUTTON_STYLE,
onclick: () => {
this.userPressed = false;
this.cancel.style.display = 'none';
},
});
document.body.appendChild(this.cancel);
}
}
class ConfigValidator {
static validate(config) {
const errors = [
typeof config.remove.uncommented !== 'boolean' && 'remove.uncommented must be a boolean',
typeof config.remove.unliked !== 'boolean' && 'remove.unliked must be a boolean',
typeof config.remove.images !== 'boolean' && 'remove.images must be a boolean',
typeof config.remove.videos !== 'boolean' && 'remove.videos must be a boolean',
(!Number.isInteger(config.targetLoadCount) || config.targetLoadCount < 1) && 'targetLoadCount must be a positive non-zero integer',
typeof config.runOn.home !== 'boolean' && 'runOn.home must be a boolean',
typeof config.runOn.profile !== 'boolean' && 'runOn.profile must be a boolean',
typeof config.runOn.social !== 'boolean' && 'runOn.social must be a boolean',
!Array.isArray(config.remove.customStrings) && 'remove.customStrings must be an array',
config.remove.customStrings.some((str) => typeof str !== 'string') && 'remove.customStrings must only contain strings',
typeof config.remove.caseSensitive !== 'boolean' && 'remove.caseSensitive must be a boolean',
!Array.isArray(config.linkedConditions) && 'linkedConditions must be an array',
config.linkedConditions.some((conditionGroup) => {
if (!Array.isArray(conditionGroup)) return true;
return conditionGroup.some((condition) => {
if (typeof condition !== 'string' && !Array.isArray(condition)) return true;
if (Array.isArray(condition)) {
return condition.some((item) => !['uncommented', 'unliked', 'images', 'videos', 'customStrings'].includes(item));
}
return !['uncommented', 'unliked', 'images', 'videos', 'customStrings'].includes(condition);
});
}) && 'linkedConditions must only contain arrays with valid strings',
].filter(Boolean);
if (errors.length > 0) {
console.error('Script configuration errors:');
errors.forEach((error) => console.error(error));
return false;
}
return true;
}
}
const SELECTORS = {
div: {
button: 'div.load-more',
activity: 'div.activity-entry',
replies: 'div.action.replies',
likes: 'div.action.likes',
},
span: {
count: 'span.count',
},
class: {
image: 'img',
video: 'video',
}
};
(function() {
'use strict';
if (!ConfigValidator.validate(config)) {
console.error('Script disabled due to configuration errors.');
return;
}
const activityHandler = new ActivityHandler();
const uiHandler = new UIHandler();
const mainApp = new MainApp(activityHandler, uiHandler);
mainApp.initializeObserver();
})();