MyAnimeList(MAL) - Search Filter

This script hides search results that you already have on your list

// ==UserScript==
// @name           MyAnimeList(MAL) - Search Filter
// @match          https://myanimelist.net/*
// @description    This script hides search results that you already have on your list
// @version        2.0.0
// @author         Cpt_mathix
// @namespace      https://greasyfork.org/users/16080
// @license        GPL-2.0-or-later; http://www.gnu.org/licenses/gpl-2.0.txt
// @run-at         document-end
// @grant          none
// @noframes
// ==/UserScript==

/* jshint esversion: 11 */

const version = "2.0.0";

const STATUS_NOTINMYLIST = 0;
const STATUS_WATCHING = 1;
const STATUS_READING = 1;
const STATUS_COMPLETED = 2;
const STATUS_ONHOLD = 3;
const STATUS_DROPPED = 4;
const STATUS_PLANNED = 6;

const userName = window.MAL.USER_NAME;
if (!userName) return;

let animeList, mangaList;
init();

async function init() {
    animeList = await getUserList("anime");
    mangaList = await getUserList("manga");

    await initEditBoxes();
    initMyListFilter();
}

async function initEditBoxes() {
    // Detect all edit boxes
    var editBoxes = document.querySelectorAll("a.button_edit");

    for (let editBox of editBoxes) {
        const match = editBox.href.match(/\/ownlist\/(anime|manga)\/(\d+)\//);
        if (!match) continue;
        const [, type, id] = match;

        // only change editboxes with the text "edit"
        // some edit boxes already have CW or Watching, we don't want to update these
        if (editBox.children.length === 0 || editBox.children[0].textContent !== "edit") {
            continue;
        }

        if (type === "anime") {
            const anime = await getAnimeFromList(id);
            switch (anime?.status) {
                case STATUS_WATCHING:
                    editBox.classList.add("watching");
                    editBox.children[0].textContent = "CW";
                    break;
                case STATUS_COMPLETED:
                    editBox.classList.add("completed");
                    editBox.children[0].textContent = "CMPL";
                    break;
                case STATUS_ONHOLD:
                    editBox.classList.add("on-hold");
                    editBox.children[0].textContent = "HOLD";
                    break;
                case STATUS_DROPPED:
                    editBox.classList.add("dropped");
                    editBox.children[0].textContent = "DROP";
                    break;
                case STATUS_PLANNED:
                    editBox.classList.add("plantowatch");
                    editBox.children[0].textContent = "PTW";
                    break;
            }
        }

        if (type === "manga") {
            const manga = await getMangaFromList(id);
            switch (manga?.status) {
                case STATUS_WATCHING:
                    editBox.classList.add("reading");
                    editBox.children[0].textContent = "CR";
                    break;
                case STATUS_COMPLETED:
                    editBox.classList.add("completed");
                    editBox.children[0].textContent = "CMPL";
                    break;
                case STATUS_ONHOLD:
                    editBox.classList.add("on-hold");
                    editBox.children[0].textContent = "HOLD";
                    break;
                case STATUS_DROPPED:
                    editBox.classList.add("dropped");
                    editBox.children[0].textContent = "DROP";
                    break;
                case STATUS_PLANNED:
                    editBox.classList.add("plantoread");
                    editBox.children[0].textContent = "PTR";
                    break;
            }
        }
    }
}

function initMyListFilter() {
    const allItems = document.querySelectorAll("a.Lightbox_AddEdit");
    const notInMyListItems = document.querySelectorAll("a.Lightbox_AddEdit.button_add, a.Lightbox_AddEdit.notinmylist");
    const watchingOrReadingItems = document.querySelectorAll("a.Lightbox_AddEdit.watching, a.Lightbox_AddEdit.reading");
    const completedItems = document.querySelectorAll("a.Lightbox_AddEdit.completed");
    const onHoldItems = document.querySelectorAll("a.Lightbox_AddEdit.on-hold");
    const droppedItems = document.querySelectorAll("a.Lightbox_AddEdit.dropped");
    const plannedItems = document.querySelectorAll("a.Lightbox_AddEdit.plantowatch, a.Lightbox_AddEdit.plantoread");

    function filterFunction(filterType, filterDisabled, elementSelectorToApplyFilter) {
        if (filterType === 'all') {
            updateDisplayNone(allItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_NOTINMYLIST) {
            updateDisplayNone(notInMyListItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_WATCHING || filterType === STATUS_READING) {
            updateDisplayNone(watchingOrReadingItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_COMPLETED) {
            updateDisplayNone(completedItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_ONHOLD) {
            updateDisplayNone(onHoldItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_DROPPED) {
            updateDisplayNone(droppedItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_PLANNED) {
            updateDisplayNone(plannedItems, filterDisabled, elementSelectorToApplyFilter);
        }
    }

    function updateDisplayNone(elements, isVisible, elementSelectorToUpdate) {
        elements.forEach(element => {
            const closestElement = element.closest(elementSelectorToUpdate);
            if (isVisible) {
                closestElement.style.display = '';
            } else {
                closestElement.style.display = 'none';
            }
        });
    }

    function recFilterFunction(filterType, filterDisabled, elementSelectorToApplyFilter) {
        if (filterType === 'all') {
            updateRecDisplayNone(allItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_NOTINMYLIST) {
            updateRecDisplayNone(notInMyListItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_WATCHING || filterType === STATUS_READING) {
            updateRecDisplayNone(watchingOrReadingItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_COMPLETED) {
            updateRecDisplayNone(completedItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_ONHOLD) {
            updateRecDisplayNone(onHoldItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_DROPPED) {
            updateRecDisplayNone(droppedItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_PLANNED) {
            updateRecDisplayNone(plannedItems, filterDisabled, elementSelectorToApplyFilter);
        }
    }

    function updateRecDisplayNone(elements, isVisible, elementSelectorToUpdate) {
        elements.forEach(element => {
            const closestElement = element.closest(elementSelectorToUpdate);
            if (isVisible) {
                closestElement.classList.remove('1_hidden');
                closestElement.classList.replace('2_hidden', '1_hidden');
                closestElement.style.display = '';
            } else {
                if (closestElement.classList.contains('1_hidden')) {
                    closestElement.classList.replace('1_hidden', '2_hidden');
                    closestElement.style.display = 'none';
                } else {
                    closestElement.classList.add('1_hidden');
                }
            }
        });
    }

    if (document.location.href.includes("myanimelist.net/topanime.php")) {
        const anchor = document.querySelector("h2.top-rank-header2");
        if (anchor) {
            const html = constructMyListFilterHTML("anime", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mt4 mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, filterFunction, '.ranking-list', fixTopRankingTableColors);
        }
    }

    if (document.location.href.includes("myanimelist.net/topmanga.php")) {
        const anchor = document.querySelector("h2.top-rank-header2");
        if (anchor) {
            const html = constructMyListFilterHTML("manga", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mt4 mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, filterFunction, '.ranking-list', fixTopRankingTableColors);
        }
    }

    if (document.location.href.includes("myanimelist.net/anime.php?") && !document.location.href.includes("_location=mal_h_m")) {
        const anchor = document.querySelector("#content > .normal_header");
        if (anchor) {
            const html = constructMyListFilterHTML("anime", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mb4 mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, filterFunction, 'tr', fixSearchTableColors);
        }
    }

    if (document.location.href.includes("myanimelist.net/manga.php?") && !document.location.href.includes("_location=mal_h_m")) {
        const anchor = document.querySelector("#content > .normal_header");
        if (anchor) {
            const html = constructMyListFilterHTML("manga", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mb4 mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, filterFunction, 'tr', fixSearchTableColors);
        }
    }

    if (document.location.href.includes("myanimelist.net/reviews.php?t=anime")) {
        const anchor = document.querySelector(".review-sort-and-filter");
        if (anchor) {
            const html = constructMyListFilterHTML("anime", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", html);
            initializeMyListFilterElement(anchor, filterFunction, '.review-element');
        }
    }

    if (document.location.href.includes("myanimelist.net/reviews.php?t=manga")) {
        const anchor = document.querySelector(".review-sort-and-filter");
        if (anchor) {
            const html = constructMyListFilterHTML("manga", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", html);
            initializeMyListFilterElement(anchor, filterFunction, '.review-element');
        }
    }

    if (document.location.href.includes("myanimelist.net/recommendations.php?s=recentrecs&t=anime")) {
        const anchor = document.querySelector("#horiznav_nav");
        if (anchor) {
            const html = constructMyListFilterHTML("anime", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, recFilterFunction, 'div.borderClass');
        }
    }

    if (document.location.href.includes("myanimelist.net/recommendations.php?s=recentrecs&t=manga")) {
        const anchor = document.querySelector("#horiznav_nav");
        if (anchor) {
            const html = constructMyListFilterHTML("manga", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, recFilterFunction, 'div.borderClass');
        }
    }

    if (/^https:\/\/myanimelist\.net\/anime\/[^\/]+\/[^\/]+\/userrecs$/.test(document.location.href)) {
        const anchor = document.querySelector("#content .rightside h2");
        if (anchor) {
            const html = constructMyListFilterHTML("anime", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, filterFunction, 'div.borderClass');
        }
    }

    if (/^https:\/\/myanimelist\.net\/manga\/[^\/]+\/[^\/]+\/userrecs$/.test(document.location.href)) {
        const anchor = document.querySelector("#content .rightside h2");
        if (anchor) {
            const html = constructMyListFilterHTML("manga", "20px", "0px");
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, filterFunction, 'div.borderClass');
        }
    }
}

function constructMyListFilterHTML(type, top, right) {
    return `
<style>
  .btn-show-mylist-filters {
    background-image: url(/images/icon-pulldown2.png?v=1634263200);
    background-position: right -15px;
    background-repeat: no-repeat;
    background-size: 8px 26px;
    color: #787878;
    cursor: pointer;
    display: inline-block;
    padding-right: 12px !important;
  }
  .dark-mode .btn-show-mylist-filters {
    color: #a3a3a3;
  }
  .dark-mode .btn-show-mylist-filters.filtered {
    background-image: url(/images/icon-pulldown3.png?v=1634263200);
  }
  .btn-show-mylist-filters.on {
    background-position: right 6px;
  }
  .mylist-filter-block {
    background-color: #fff;
    border: #d8d8d8 1px solid;
    border-radius: 0 0 4px 4px;
    -webkit-box-shadow: 1px 1px 5px rgba(0,0,0,.2);
    box-shadow: 1px 1px 5px rgba(0,0,0,.2);
    display: none;
    font-weight: 400;
    padding: 8px;
    position: absolute;
    width: 130px;
    z-index: 10;
  }
  .dark-mode .mylist-filter-block {
    background-color: #121212;
    border: #353535 1px solid;
  }
  .mylist-filter-block .btn-close {
    color: #787878;
    cursor: pointer;
    font-size: 13px;
    -webkit-transition-duration: .3s;
    transition-duration: .3s;
    -webkit-transition-property: all;
    transition-property: all;
    -webkit-transition-timing-function: ease-in-out;
    transition-timing-function: ease-in-out;
  }
  .dark-mode .mylist-filter-block .btn-close {
    color: #a3a3a3;
  }
  .mylist-filter-block .mylist-filter-block-options {
    list-style: none;
    margin: 0;
    padding: 0;
  }
  .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter {
    clear: both;
    color: #787878;
    cursor: pointer;
    display: inline-block !important;
    font-size: 11px;
    margin: 1px 0 !important;
    padding: 2px 0 4px 18px !important;
    position: relative;
    width: 120px;
    text-align: left;
  }
  .mylist-filter-block .mylist-filter-block-options .ml12.btn-mylist-filter {
    margin-left: 12px !important;
    width: 108px;
  }
  .dark-mode .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter {
    color: #a3a3a3;
  }
  .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter.fa-stack {
    height: 1.4em;
  }
  .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter .fa-square {
    color: #888;
    font-size: 1.5em;
    width: 14px;
  }
  .dark-mode .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter .fa-square {
    color: #a3a3a3;
  }
  .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter .fa-check {
    color: #080;
    font-size: 1em;
    top: -1px;
    width: 14px;
    display: none;
  }
  .dark-mode .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter .fa-check {
    color: #3dc53d;
  }
  .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter.selected .fa-check {
    display: inline-block;
  }
</style>
<span class="fl-r btn-show-mylist-filters sort fs11 fw-n" data-id="mylist">My List</span>
<div class="mylist-filter-block sort" style="display: none; top: ${top}; right: ${right};">
  <span class="fl-r btn-close">
    <i class="fa-solid fa-times"></i>
  </span>
  <ul class="mylist-filter-block-options" id="mylist">
    <li class="btn-mylist-filter fa-stack selected" data-status="all">
      <i class="fa-regular fa-square fa-stack-2x"></i>
      <i class="fa-solid fa-check fa-stack-1x"></i> All
    </li>
    <li class="ml12 btn-mylist-filter fa-stack selected" data-status="0">
      <i class="fa-regular fa-square fa-stack-2x"></i>
      <i class="fa-solid fa-check fa-stack-1x"></i> Not In My List
    </li>
    <li class="ml12 btn-mylist-filter fa-stack selected" data-status="1">
      <i class="fa-regular fa-square fa-stack-2x"></i>
      <i class="fa-solid fa-check fa-stack-1x"></i> ${type === "anime" ? "Watching" : "Reading"}
    </li>
    <li class="ml12 btn-mylist-filter fa-stack selected" data-status="2">
      <i class="fa-regular fa-square fa-stack-2x"></i>
      <i class="fa-solid fa-check fa-stack-1x"></i> Completed
    </li>
    <li class="ml12 btn-mylist-filter fa-stack selected" data-status="3">
      <i class="fa-regular fa-square fa-stack-2x"></i>
      <i class="fa-solid fa-check fa-stack-1x"></i> On-Hold
    </li>
    <li class="ml12 btn-mylist-filter fa-stack selected" data-status="4">
      <i class="fa-regular fa-square fa-stack-2x"></i>
      <i class="fa-solid fa-check fa-stack-1x"></i> Dropped
    </li>
    <li class="ml12 btn-mylist-filter fa-stack selected" data-status="6">
      <i class="fa-regular fa-square fa-stack-2x"></i>
      <i class="fa-solid fa-check fa-stack-1x"></i> ${type === "anime" ? "Plan to Watch" : "Plan to Read"}
    </li>
  </ul>
</div>`;
}

function initializeMyListFilterElement(anchor, filterAction, elementSelectorToApplyFilter, customAction) {
    const filterToggle = anchor.querySelector('.btn-show-mylist-filters'); // The span to toggle filters
    const filterBlock = anchor.querySelector('.mylist-filter-block'); // The block with filter options
    const closeButton = filterBlock.querySelector('.btn-close'); // The close button inside the filter block
    const listItems = filterBlock.querySelectorAll('.btn-mylist-filter'); // All li elements in the list

    // Function to load selected filter states from localStorage
    function loadFiltersFromLocalStorage() {
        const storedFilters = getSetting('MyListFilters') || {};

        // Set the selected state for each filter from localStorage
        listItems.forEach(item => {
            const status = item.dataset.status;
            if (storedFilters[status] === false) {
                item.classList.remove('selected');
                if (status !== 'all') {
                    filterAction(item.dataset.status, false, elementSelectorToApplyFilter);
                }
            } else {
                item.classList.add('selected');
            }
        });

        if (storedFilters.all === false) {
            filterToggle.classList.add('filtered');
        }

        customAction?.();
    }

    // Function to save selected filter states to localStorage
    function saveFiltersToLocalStorage() {
        const filtersState = {};

        listItems.forEach(item => {
            const status = item.dataset.status;
            filtersState[status] = item.classList.contains('selected');
        });

        // Store the filters state in localStorage
        saveSetting('MyListFilters', filtersState);
    }

    // Toggle the visibility of the filter block when the span is clicked
    filterToggle.addEventListener('click', function() {
        // Toggle the "on" class on the filterToggle (to show/hide the filter)
        filterToggle.classList.toggle('on');

        // Toggle the display of the filter block
        if (filterToggle.classList.contains('on')) {
            filterBlock.style.display = 'block';
        } else {
            filterBlock.style.display = 'none';
        }
    });

    // Close the filter block when the close button is clicked
    closeButton.addEventListener('click', function() {
        filterToggle.classList.remove('on');
        filterBlock.style.display = 'none';
    });

    // Close the filter block when clicking outside of the filter block
    document.addEventListener('click', function(event) {
        // Check if the click is outside the filter block and the toggle button
        if (!filterBlock.contains(event.target) && event.target !== filterToggle) {
            filterToggle.classList.remove('on');
            filterBlock.style.display = 'none';
        }
    });

    // Toggle the "selected" class on li elements
    listItems.forEach(function(item) {
        item.addEventListener('click', function() {
            // If the "All" filter is clicked, handle it separately
            if (item.dataset.status === 'all') {
                item.classList.toggle('selected');
                item.classList.toggle('filtered');

                // If "All" is selected, select all other filters
                if (item.classList.contains('selected')) {
                    listItems.forEach(i => {
                        if (i.dataset.status !== 'all') {
                            i.classList.add('selected');
                        }
                    });
                } else {
                    // If "All" is unselected, unselect all other filters
                    listItems.forEach(i => {
                        if (i.dataset.status !== 'all') {
                            i.classList.remove('selected');
                        }
                    });
                }
            } else {
                item.classList.toggle('selected');

                // Check if "All" filter should be updated
                const allSelected = [...listItems].every(item => item.dataset.status === 'all' || item.classList.contains('selected'));
                const allItem = filterBlock.querySelector('[data-status="all"]');

                if (allSelected) {
                    allItem.classList.add('selected');
                    filterToggle.classList.remove('filtered');
                } else {
                    allItem.classList.remove('selected');
                    filterToggle.classList.add('filtered');
                }
            }

            saveFiltersToLocalStorage();
            filterAction(item.dataset.status, item.classList.contains('selected'), elementSelectorToApplyFilter);
            customAction?.();
        });
    });

    loadFiltersFromLocalStorage();
}

function fixTopRankingTableColors() {
    const table = document.querySelector(".top-ranking-table");
    if (!document.getElementById('custom-top-ranking-table-style')) {
        table.insertAdjacentHTML("beforebegin", `<style id="custom-top-ranking-table-style">
  .top-ranking-table tr.ranking-list td {
    background-color: #fff !important;
  }

  .top-ranking-table tr.ranking-list.odd-row td {
    background-color: #f8f8f8 !important;
  }

  .dark-mode .top-ranking-table tr.ranking-list td {
    background-color: #121212 !important;
  }

  .dark-mode .top-ranking-table tr.ranking-list.odd-row td {
    background-color: #181818 !important;
  }
</style>`);
    }

    const tableRows = table.querySelectorAll("tr.ranking-list");
    const visibleRows = Array.from(tableRows).filter(row => row.style.display !== 'none');
    visibleRows.forEach((row, index) => {
        row.classList.remove('odd-row');
        if (index % 2 === 0) {
            row.classList.add('odd-row');
        }
    });
}

function fixSearchTableColors() {
    const table = document.querySelector("#content > .list table");
    const tableRows = table.querySelectorAll("tr");
    const visibleRows = Array.from(tableRows).filter(row => row.style.display !== 'none');
    visibleRows.forEach((row, index) => {
        var tableRowColumns = row.querySelectorAll("td");
        Array.from(tableRowColumns).forEach(column => {
            column.classList.remove('bgColor0', 'bgColor1');
            if (index % 2 === 0) {
                column.classList.add('bgColor1');
            } else {
                column.classList.add('bgColor0');
            }
        });
    });
}

async function getAnimeFromList(id) {
    var anime = animeList[id];

    if (!anime) {
        animeList = await getUserList("anime", true);
    }

    return animeList[id];
}

async function getMangaFromList(id) {
    var manga = mangaList[id];

    if (!manga) {
        mangaList = await getUserList("manga", true);
    }

    return mangaList[id];
}

async function getUserList(type, forceRefresh = false) {
    let userlistWrapper = getSetting(type + 'list', false);

    // Fetch userlist if it is older than 1 hour
    if (forceRefresh || (!(userlistWrapper?.fetchDate && ((new Date() - new Date(userlistWrapper.fetchDate)) / (60*60*1000) < 1)))) {
        const userlist = await fetchUserList(type);
        userlistWrapper = {
            "userlist": userlist,
            "fetchDate": new Date()
        };
        saveSetting(type + 'list', userlistWrapper, false);
    }

    return flatten(userlistWrapper.userlist, type === "anime" ? "anime_id" : "manga_id");
}

async function fetchUserList(type, userlist = [], page = 1) {
    await fetch('https://myanimelist.net/' + type + 'list/' + userName + '/load.json?offset=' + ((page - 1) * 300)).then(function(response) {
        return response.json();
    }).then(async function(json) {
        userlist = userlist.concat(json);

        if (json.length !== 0) {
            await timeout(1000);
            userlist = await fetchUserList(type, userlist, ++page);
        }
    });

    return userlist;
}

function saveSetting(key, value, hasVersion = true) {
    localStorage.setItem('MAL#' + key + (hasVersion ? '_' + version : ''), JSON.stringify(value));
}

function getSetting(key, hasVersion = true) {
    const value = localStorage.getItem('MAL#' + key + (hasVersion ? '_' + version : ''));
    if (value) {
        return JSON.parse(value);
    } else {
        return null;
    }
}

function flatten(list, itemKey) {
    const map = {};
    for (let item of list) {
        map[item[itemKey]] = item;
    }
    return map;
}

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}