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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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