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