MyAnimeList(MAL) - Search Filter

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

目前为 2025-04-27 提交的版本。查看 最新版本

// ==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.2
// @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.2";

const STATUS_NOTINMYLIST = 0;
const STATUS_INMYLIST = 10;
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 initializing = true;
let animeList, mangaList;
let allItems,
    inMyListItems,
    notInMyListItems,
    watchingOrReadingItems,
    completedItems,
    onHoldItems,
    droppedItems,
    plannedItems;
let globalLoadFiltersFromLocalStorage;

init();

async function init() {
    const hasMyListFilter = initMyListFilter();

    animeList = await getUserList("anime");
    mangaList = await getUserList("manga");

    await initEditBoxes();

    if (!hasMyListFilter) return;

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

    initializing = false;
    document.getElementById("mylist-filters").textContent = "My List";
    globalLoadFiltersFromLocalStorage?.();
}

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() {
    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') {
            updateDisplayNone(allItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_INMYLIST) {
            updateRecDisplayNone('inmylist', inMyListItems, filterDisabled, elementSelectorToApplyFilter);
        } else if (+filterType === STATUS_NOTINMYLIST) {
            updateRecDisplayNone('notinmylist', 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);
        }
    }

    // Only used in recommendations page (not in my list will only filter if both recommendations are not on your list)
    function updateRecDisplayNone(key, elements, isVisible, elementSelectorToUpdate) {
        elements.forEach(element => {
            const closestElement = element.closest(elementSelectorToUpdate);
            if (isVisible) {
                closestElement.classList.remove(key + '1_hidden');
                closestElement.classList.replace(key + '2_hidden', key + '1_hidden');
                closestElement.style.display = '';
            } else {
                if (closestElement.classList.contains(key + '1_hidden')) {
                    closestElement.classList.replace(key + '1_hidden', key + '2_hidden');
                    closestElement.style.display = 'none';
                } else {
                    closestElement.classList.add(key + '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', 'top-anime', fixTopRankingTableColors);
            return true;
        }
    }

    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', 'top-manga', fixTopRankingTableColors);
            return true;
        }
    }

    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', 'anime-search', fixSearchTableColors);
            return true;
        }
    }

    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', 'manga-search', fixSearchTableColors);
            return true;
        }
    }

    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', 'anime-reviews');
            return true;
        }
    }

    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', 'manga-reviews');
            return true;
        }
    }

    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", true);
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, recFilterFunction, 'div.borderClass', 'anime-recommendations');
            anchor.insertAdjacentHTML("beforeend", `<span class="fl-r mr4" title="The 'My List' filters work a bit different on this page, 'Both In My List' & 'Both Not In My List' will only hide the recommendation if both anime entries are (not) in your list. The other filters will hide the recommendation if either anime entry matches the filter.">ⓘ</span>`);
            return true;
        }
    }

    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", true);
            anchor.insertAdjacentHTML("beforeend", `<div class="fl-r di-ib mr12 po-r">${html}</div>`);
            initializeMyListFilterElement(anchor, recFilterFunction, 'div.borderClass', 'manga-recommendations');
            anchor.insertAdjacentHTML("beforeend", `<span class="fl-r mr4" title="The 'My List' filters work a bit different on this page, 'Both In My List' & 'Both Not In My List' will only hide the recommendation if both manga entries are (not) in your list. The other filters will hide the recommendation if either manga entry matches the filter.">ⓘ</span>`);
            return true;
        }
    }

    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', 'anime-userrecs');
            return true;
        }
    }

    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', 'manga-userrecs');
            return true;
        }
    }

    return false;
}

function constructMyListFilterHTML(type, top, right, isRecommendations = false) {
    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;
  }
  .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter .fa-xmark {
    color: #f20;
    font-size: 1em;
    top: -1px;
    width: 14px;
    display: none;
  }
  .dark-mode .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter .fa-xmark {
    color: #ff8462;
  }
  ${isRecommendations ? `
  .mylist-filter-block .mylist-filter-block-options .btn-mylist-filter:not(.selected) .fa-xmark {
    display: inline-block;
  }` : ''}
</style>
<span class="fl-r btn-show-mylist-filters sort fs11 fw-n" data-id="mylist" id="mylist-filters">Loading...</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>
    ${isRecommendations ? `
    <li class="ml12 btn-mylist-filter fa-stack selected" data-status="10">
      <i class="fa-regular fa-square fa-stack-2x"></i>
      <i class="fa-solid fa-check fa-stack-1x"></i> Both In My List
    </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> ${isRecommendations ? 'Both' : ''} 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-xmark fa-stack-1x"></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-xmark fa-stack-1x"></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-xmark fa-stack-1x"></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-xmark fa-stack-1x"></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-xmark fa-stack-1x"></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, filterName, customAction = null) {
    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#' + filterName) || {};

        // 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#' + filterName, filtersState);
    }

    // Toggle the visibility of the filter block when the span is clicked
    filterToggle.addEventListener('click', function() {
        if (initializing) return;

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

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