Unlimited MAL Ignore list

Ignore an unlimited amount of users with other custom settings: deleting the entire post, replace the content of the message with a custom message, replace or delete the avatar, keep or delete the signature.

目前为 2021-10-12 提交的版本。查看 最新版本

// ==UserScript==
// @name         Unlimited MAL Ignore list
// @namespace    http://tampermonkey.net/
// @version      0.60
// @description  Ignore an unlimited amount of users with other custom settings: deleting the entire post, replace the content of the message with a custom message, replace or delete the avatar, keep or delete the signature.
// @author       Only_Brad
// @match        https://myanimelist.net/*
// @run-at document-end
// ==/UserScript==

(function() {
    const POSTS_URL = "forum/?topicid";
    const TOPICS_URL = "forum/?board";
    const CLUB_TOPICS_URL_1 = "clubs.php";
    const CLUB_TOPICS_URL_2 = "forum/?clubid";
    const BLACKLIST_URL = "https://myanimelist.net/blacklist";

    const BLACKLIST_KEY = "ignore-list";
    const SETTINGS_KEY = "ignore-list-settings";

    const POST_USERS_SELECTOR = ".forum_boardrow2 > div";
    const TOPIC_USERS_SELECTOR = ".forum_postusername a";
    const MESSAGE_SELECTOR = ".forum_boardrow1 [id^=message]";
    const AVATAR_SELECTOR = ".forum-icon";
    const USER_PROFILE_SELECTOR = "[href^='/profile']";
    const USER_INFO_SELECTOR = "[id^=messageuser]";
    const SIGNATURE_SELECTOR = ".sig";
    const FORUM_MESSAGE_SELECTOR = "[id^=forumMsg]";
    const ACTION_BAR_SELECTOR = "[id^=postEditButtons]";

    const IGNORE_POSTS = 0;
    const REPLACE_POSTS = 1;
    const DO_NOTHING_POSTS = 2;

    let blacklist;
    let settings;

    //routing
    if (window.location.href.includes(POSTS_URL)) {
        handlePosts();
    } else if (
        window.location.href.includes(TOPICS_URL) ||
        window.location.href.includes(CLUB_TOPICS_URL_1) ||
        window.location.href.includes(CLUB_TOPICS_URL_2)
    ) {
        handleTopics();
    } else if (window.location.href === BLACKLIST_URL) {
        handleBlacklist();
    }

    //GM_addStyle equivalent that works on firefox
    function addStyle(css) {
        const style = document.getElementById("addStyleBy8626") || (function() {
            const style = document.createElement('style');
            style.type = 'text/css';
            style.id = "addStyleBy8626";
            document.head.appendChild(style);
            return style;
        })();
        style.innerHTML += css;
    }

    //helper functions to load from localStorage
    function loadBlackList() {
        blacklist = JSON.parse(localStorage.getItem(BLACKLIST_KEY)) || [];
    }

    function saveBlackList() {
        localStorage.setItem(BLACKLIST_KEY, JSON.stringify(blacklist));
    }

    function loadSettings() {
        settings = JSON.parse(localStorage.getItem(SETTINGS_KEY)) || {
            replaceAvatar: false,
            removeSignatures: true,
            postMode: IGNORE_POSTS,
            removeTopics: true,
            customPost: "",
            customAvatar: ""
        };
    }

    function saveSetting(key, value) {
        settings[key] = value;
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
    }

    //functions called by the routers
    function handlePosts() {
        loadBlackList();
        loadSettings();

        switch (settings.postMode) {
            case IGNORE_POSTS:
                ignorePosts();
                return;
            case REPLACE_POSTS:
                replacePosts();
                break;
            default:
                break;
        }

        addBlackListButtons();
        if (settings.replaceAvatar) replaceAvatar();
        if (settings.removeSignatures) removeSignatures();
    }

    function handleTopics() {
        loadBlackList();
        loadSettings();

        if (settings.removeTopics) removeTopics();
    }

    function handleBlacklist() {
        loadBlackList();
        loadSettings();

        document.title = "Blacklist - MyAnimeList.net";

        //remove the 404 stuff
        document.querySelector("h1").textContent = "Ignore List";
        document.querySelector(".error404").remove();

        //CSS
        addStyle(".user{display:flex;margin:10px}.name{margin-right:20px}.name{border-bottom:solid #000 1px}.name[contenteditable]{min-width:100px;border-bottom:solid #000 1px}.name[contenteditable]:focus{border:none;outline:solid red 5px}.page-common #content{display:flex;justify-content:center;}.settings{display:flex;gap:25px;}.settings>*{padding: 25px;}.customPost{width:100% !important;}");

        //HTML for the blacklist
        document.getElementById("content").innerHTML =
            `<div data-blacklist class="black-list"></div>
        <div data-add-user class="add-user">
            <div data-user class="user">
                <div data-name class="name" contenteditable="true" onclick="this.focus()"></div>
                <button data-add class="add">Add</div>
            </div>
        </div>`

        //HTML for the settings
        const settings = document.createElement("div");
        settings.innerHTML = `
            <h2>Settings</h2>
            <form>
                <div class="settings">
                    <div class="posts">
                        <h3>Posts</h3>
                        <div class="form-check">
                            <input class="form-check-input" type="radio" name="posts" id="doNothingPosts" data-clickable-setting="doNothingPosts">
                            <label class="form-check-label" for="doNothingPosts">
                                Do Nothing
                            </label>
                        </div>
                        <div class="form-check">
                            <input class="form-check-input" type="radio" name="posts" id="hidePosts" data-clickable-setting="hidePosts">
                            <label class="form-check-label" for="hidePosts">
                                Hide Posts
                            </label>
                        </div>
                        <div class="form-check">
                            <input class="form-check-input" type="radio" name="posts" id="replacePosts" data-clickable-setting="replacePosts">
                            <label class="form-check-label" for="replacePosts">
                                Replace posts with a custom message
                            </label>
                        </div>
                        <textarea class="form-control customPost" name="customPost" id="customPost" data-text-setting="customPost"></textarea>
                    </div>
                    <div class="avatar">
                        <h3>Posts extra options</h3>
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" name="replaceAvatar" id="replaceAvatar" data-clickable-setting="replaceAvatar">
                            <label class="form-check-label" for="replaceAvatar">
                                Replace avatars with a custom avatar
                            </label>
                            <input class="form-control" type="text" name="customAvatar" id="customAvatar" data-text-setting="customAvatar">
                            <br>
                            <small>Leave it empty to remove the avatar</small>
                        </div>
                        <div class="form-check" style="margin-top: 10px;">
                            <input class="form-check-input" type="checkbox" name="removeSignatures" id="removeSignatures" data-clickable-setting="removeSignatures">
                            <label class="form-check-label" for="removeSignatures">
                                Hide the signature
                            </label>
                        </div>
                        <small style="margin-top: 20px; display: block;"><strong>These settings have no effect if the Posts setting is set to "Hide Posts"</strong></small>
                    </div>
                    <div class="topics">
                        <h3>Topics</h3>
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" name="removeTopics" id="removeTopics" data-clickable-setting="removeTopics">
                            <label class="form-check-label" for="removeTopics">
                                Hide Topics
                            </label>
                        </div>
                    </div>
                </div>
            </form>`;

        document.getElementById("content").insertAdjacentElement("afterend", settings);
        document.querySelector("[data-add]").addEventListener("click", addNode);
        document.querySelectorAll("[data-clickable-setting]").forEach(clickable => {
            clickable.addEventListener("click", clickedSetting);
        });
        document.querySelectorAll("[data-text-setting]").forEach(text => {
            text.addEventListener("input", textSetting);
        });
        blacklist.forEach(createNode);
        loadSettingsIntoInputs();
    }

    function clickedSetting(e) {
        const input = e.target;

        switch (input.dataset.clickableSetting) {
            case "doNothingPosts":
                saveSetting("postMode", DO_NOTHING_POSTS);
                break;
            case "hidePosts":
                saveSetting("postMode", IGNORE_POSTS);
                break;
            case "replacePosts":
                saveSetting("postMode", REPLACE_POSTS);
                break;
            case "replaceAvatar":
                saveSetting("replaceAvatar", input.checked);
                break;
            case "removeTopics":
                saveSetting("removeTopics", input.checked);
                break;
            case "removeSignatures":
                saveSetting("removeSignatures", input.checked);
                break;
            default:
                return;
        }
    }

    function textSetting(e) {
        const input = e.target;
        switch (input.dataset.textSetting) {
            case "customPost":
                saveSetting("customPost", input.value);
                break;
            case "customAvatar":
                saveSetting("customAvatar", input.value);
                break;
            default:
                return;
        }
    }

    function loadSettingsIntoInputs() {
        switch (settings.postMode) {
            case DO_NOTHING_POSTS:
                document.getElementById("doNothingPosts").checked = true;
                break;
            case IGNORE_POSTS:
                document.getElementById("hidePosts").checked = true;
                break;
            case REPLACE_POSTS:
                document.getElementById("replacePosts").checked = true;
                break;
        }

        if (settings.removeTopics) {
            document.getElementById("removeTopics").checked = true;
        }
        if (settings.removeSignatures) {
            document.getElementById("removeSignatures").checked = true;
        }
        document.getElementById("customPost").value = settings.customPost || "";
        document.getElementById("customAvatar").value = settings.customAvatar || "";
    }

    function alterPosts(action) {
        document.querySelectorAll(POST_USERS_SELECTOR).forEach(user => {
            if (!blacklist.includes(user.querySelector("strong").textContent)) return;
            let post = user.parentNode;
            for (let i = 0; i < 4; i++) {
                post = post.parentNode;
            }
            action(post, user);
        });
    }

    function removeTopics() {
        document.querySelectorAll(TOPIC_USERS_SELECTOR).forEach(user => {
            if (!blacklist.includes(user.textContent)) return;
            user.closest("tr").style.display = "none";
        });
    }

    function ignorePosts() {
        alterPosts(post => {
            post.style.display = "none";
            post.previousElementSibling.style.display = "none";
        });
    }

    function replacePosts() {
        alterPosts(post => {
            const message = post.querySelector(MESSAGE_SELECTOR);
            message.innerHTML = settings.customPost;
        });
    }

    function replaceAvatar() {
        alterPosts((post, user) => {
            const avatar = user.querySelector(AVATAR_SELECTOR);

            if (!avatar) {
                if (settings.customAvatar === "") return;

                const avatar = document.createElement("a");
                avatar.href = user.querySelector(USER_PROFILE_SELECTOR).href
                avatar.className = "forum-icon";
                avatar.innerHTML = `
                    <img class=" lazyloaded" data-src="${settings.customAvatar}" vspace="2" border="0" src="${settings.customAvatar}" width="100" height="125">`;
                user.querySelector(USER_INFO_SELECTOR).insertAdjacentElement('afterend', avatar);
            } else {
                if (settings.customAvatar === "") {
                    avatar.style.display = "none";
                    return;
                }

                const img = avatar.querySelector("img");
                img.src = settings.customAvatar;
                img.setAttribute("data-src", settings.customAvatar);
                img.setAttribute("width", 100);
                img.setAttribute("height", 125);
            }
        });
    }

    function removeSignatures() {
        alterPosts(post => {
            const signature = post.querySelector(SIGNATURE_SELECTOR);
            if (!signature) return;
            signature.style.display = "none";
        });
    }

    function addBlackListButtons() {
        document.querySelectorAll(FORUM_MESSAGE_SELECTOR).forEach(forumMessage => {
            const actionBar = forumMessage.querySelector(ACTION_BAR_SELECTOR);
            const a = document.createElement("a");
            a.href = "#!";
            a.textContent = "Blacklist User";
            a.dataset.username = forumMessage.querySelector("strong").textContent;
            a.onclick = blacklistUser;

            actionBar.prepend(document.createTextNode(" - "));
            actionBar.prepend(a);
        });
    }

    function blacklistUser(e) {
        addUser(e.target.dataset.username);
        window.location.reload();
    }

    //Add a user to the blacklist if its not already there
    function addUser(username) {
        blacklist.push(username);
        saveBlackList();
    }

    //Remove a user from the blacklist if it's there
    function removeUser(userName) {
        blacklist = blacklist.filter(name => userName !== name);
        saveBlackList();
    }

    //remove the user node from the html code and then update the localStorage
    function removeNode(e) {
        const row = e.target.parentNode;
        const name = row.querySelector("[data-name]").textContent;
        row.remove();
        removeUser(name);
    }

    //modify the user node from the html code and then update the localStorage
    function saveNode(e) {
        const newName = e.target.textContent;
        const previousName = e.target.dataset.previousName;

        previousName && removeUser(previousName);

        if (newName !== "") {
            addUser(newName);
            e.target.dataset.previousName = newName;
        } else {
            e.target.parentNode.remove();
        }
    }

    //add a new user node to the html code and then update the localStorage
    function addNode(e) {
        const node = e.target.parentNode;
        const usernameNode = node.querySelector("[data-name]");
        const username = usernameNode.textContent;
        usernameNode.textContent = "";

        if (!blacklist.includes(username)) {
            createNode(username);
            addUser(username);
        }
    }

    //create the user node then add it the html code
    function createNode(username) {
        const newUser = document.createElement("div");
        newUser.setAttribute("data-user", "");
        newUser.className = "user";
        newUser.innerHTML = `<div data-name class="name" contenteditable="true" onclick="this.focus()" data-previous-name="${username}">${username}</div>
        <button data-remove class="remove">Remove</button>`;
        newUser.querySelector("[data-name]").addEventListener("focusout", saveNode);
        newUser.querySelector("[data-remove]").addEventListener("click", removeNode);
        document.querySelector("[data-blacklist]").append(newUser);
    }
})();