Greasy Fork

Anilist: Hide Unwanted Activity

Customize activity feeds by removing unwanted entries.

目前为 2023-09-29 提交的版本。查看 最新版本

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