Greasy Fork is available in English.

MAL tierlist

make tierlist on seasonal anime page (V0.5 ordered tierlist instead of automatic sort)

// ==UserScript==
// @name         MAL tierlist
// @namespace    pepe
// @version      0.5.2
// @description  make tierlist on seasonal anime page (V0.5 ordered tierlist instead of automatic sort)
// @author       pepe
// @match        https://myanimelist.net/anime/season*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=myanimelist.net
// @require      https://code.jquery.com/ui/1.13.2/jquery-ui.js
// @license      MIT
// @grant        none
// ==/UserScript==

/* global $ */
/* global html2canvas */

(function() {

// for image downloading but doesnt work yet, so its not included
// @require https://html2canvas.hertzen.com/dist/html2canvas.js

  // fix padding in header to fit tierlist in older seasons
  if(document.querySelectorAll('.navtab')[0].textContent == 'Current Season'){
      document.head.innerHTML += `<style>
      .dark-mode .navi-seasonal .horiznav_nav ul li a {
         padding: 5px 8px;
      }
      </style>`
  }

  let tierlist_html = `
    <div id="tierlist">

  <style>
  :root {
    --image-width: 80px;
    --image-height: 113px;
  }
  ul.droptrue { list-style-type: none; margin: 0px; float: left; margin-right: 0px; padding: 0px; min-width: 100%; min-height: 80px;}
  ul.droptrue li { float: left; margin: 2px; font-size: 16px; height: var(--image-height); overflow: hidden;}
  .grid-container {
    margin-top: 2px;
    display: none;
    grid-template-columns: 1fr 11fr;
    grid-gap: 0px;
  }
  .grid-container1 {
    grid-template-columns: 1fr 4fr 3fr 2fr 2fr;
  }
  .grid-container2 {
    grid-template-columns: 1fr 11fr;
  }
.textarea {
  display: flex; align-items: center; text-align: center; height: var(--image-height); width: auto; padding: 0px!important; border: 0px!important; justify-content: center;
}
ul {display: block;
    list-style-type: disc;
    margin-block-start: 0;
    margin-block-end: 0;
    margin-inline-start: 0px;
    margin-inline-end: 0px;
    padding-inline-start: 0;
    margin: 2px;
    padding: 0;
}
.dropfalse div { height: 100%; font-size:14px!important;width:90px;}

#tier_s div, #header_s div { background-color: #2e51a2;}
#tier1 div, #header_1 div { background-color: green;}
#tier2 div, #header_2 div { background-color: oklch(0.70 0.18 126.62 / 1);}
#tier3 div, #header_3 div { background-color: oklch(0.75 0.2 70.66 / 1);}
#tier4 div { background-color: orangered;}
#tier5 div { background-color: darkred;}
  </style>

<a href="#" style="position: absolute; right: 0; padding-top: 30px;"
    onClick="
      event.preventDefault();
      document.querySelector('#contentWrapper').style.display = 'block';
      document.querySelectorAll('.grid-container')[0].style.display = 'none';
      document.querySelectorAll('.grid-container')[1].style.display = 'none';
      document.documentElement.scrollTop = localStorage.getItem('scrollPosition');
    "
>CLOSE VIEW</a>

<a id="deletetierlist" href="#" style="position: absolute; right: 0; padding-top: 7px; color: orange;" >DELETE SAVE</a>


${generateTiers()}
<!--
<ul id="tier_s" class="dropfalse">
 <div class="textarea" contenteditable="true">S</div>
</ul>
<ul id="s" class="droptrue"></ul>

<ul id="tier1" class="dropfalse">
 <div class="textarea" contenteditable="true">A</div>
</ul>
<ul id="sortable1" class="droptrue"></ul>

<ul id="tier2" class="dropfalse">
  <div class="textarea" contenteditable="true">B</div>
</ul>
<ul id="sortable2" class="droptrue"></ul>

<ul id="tier3" class="dropfalse">
  <div class="textarea" contenteditable="true">C</div>
</ul>
<ul id="sortable3" class="droptrue"></ul>

<ul id="tier4" class="dropfalse">
  <div class="textarea" contenteditable="true">D</div>
</ul>
<ul id="sortable4" class="droptrue"></ul>

<ul id="tier5" class="dropfalse">
  <div class="textarea" contenteditable="true">E</div>
</ul>
<ul id="sortable5" class="droptrue"></ul>
-->


<!-- this grid not in image -->
<div class="grid-container grid-container2">

<ul id="tier_last" class="dropfalse">
  <div class="textarea" contenteditable="true">F</div>
</ul>
<ul id="last" class="droptrue"></ul>

</div>

</div>
`
function generateTiers(){
    let tiers = 'S,A,B,C,D,E'

    let html = tiers.split(/[, ]/).map((tier, i) =>
`<ul id="tier${i==0?'_s':i}" class="dropfalse">
     <div class="textarea" contenteditable="true">${tier}</div>
</ul>
<ul id="${i==0?'s':'sortable'+i}" class="droptrue"></ul>`)

    return `<div class="grid-container">`+html.join('\n')+`</div>`
}
function generateGridTiers(){
    let tiers = 'S,A,B,C,D,E'

    let html = `<div class="grid-container grid-container1"><ul id="tier_header" class="dropfalse" style="height: var(--image-height)">
</ul>
<ul id="header_s" class="dropfalse"><div class="textarea" contenteditable="true">Carry</div></ul>
<ul id="header_1" class="dropfalse"><div class="textarea" contenteditable="true">Tactical</div></ul>
<ul id="header_2" class="dropfalse"><div class="textarea" contenteditable="true">Dark Horse</div></ul>
<ul id="header_3" class="dropfalse"><div class="textarea" contenteditable="true">Misc</div></ul>`

   html += tiers.split(/[, ]/).map((tier, i) =>
`<ul id="tier${i==0?'_s':i}" class="dropfalse">
     <div class="textarea" contenteditable="true">${tier}</div>
</ul>
${createColumns(tier)}`).join('\n')

    return html+`</div>`
}
    function createColumns(tier){
        return Array.from(Array(4)).map((v,i) => `<ul id="${tier==0?'s'+i:`sortable${i}${tier}`}" class="droptrue"></ul>`).join('\n')
    }
    // <div class="button">
    //   <a id="download" href="#">Download button</a>
    // </div>

    create_tierlist_button:{

        let tierlist_button = document.createElement(`li`)
        tierlist_button.innerHTML = `<a id="createtier" href="#" class="navtab ">Tierlist</a>`
        tierlist_button.onclick = function(event){
            event.preventDefault(); // disable default link click, interferes with scroll
            RenderTierlist();
        }
        let seasonal_header = document.querySelector('.horiznav_nav ul')
        seasonal_header.append(tierlist_button);

    }

    function RenderTierlist(){
        localStorage.setItem('scrollPosition', document.documentElement.scrollTop)

        // prevent duplicate from being rendered, insert html
        try{document.getElementById('tierlist').remove();}catch(e){}
        document.querySelector('#contentWrapper').insertAdjacentHTML("afterend", tierlist_html);

        // hide seasonal, show tier
        document.querySelector('#contentWrapper').style.display = 'none';
        document.querySelectorAll('.grid-container')[0].style.display = 'grid';
        document.querySelectorAll('.grid-container')[1].style.display = 'grid';

        // jqueryui sortable
        $( "ul.droptrue" ).sortable({
            connectWith: "ul.droptrue"
        });

        // loop seasonal images and put them to tierlist
        let new_shows = document.querySelectorAll('.js-seasonal-anime-list-key-1:nth-child(1) .js-anime-type-1 img')
        let season = document.querySelector("#content .season_nav .on").textContent
        let tierlist = JSON.parse(localStorage.getItem(season));
        let tiersOrdered = Array.from({length: tierlist?.tiers?.length}, i => []);

        // create ordered list
        for(let anime of new_shows){
            let id = anime.parentElement.href.match(/(?<=anime\/)\d+/)[0]
            let members = anime.parentElement.parentElement.parentElement.querySelector('.member').textContent
            members = members.match(/[\d.]+/) * ( members.match(/M/) ? 1000000 : ( members.match(/K/) ? 1000 : 1 ) )
            if(members < 3000) continue; // skip if low members

            if(tierlist && tierlist[id]){ // old tierlist by sort
                document.querySelector('#'+tierlist[id]).innerHTML += `<li id="${id}" class="">${anime.outerHTML.replace('167', '80')}</li>`
            }else if(tierlist && tierlist.version == 2 && tierlist.tiers.flat().includes(id)){ // new ordered tierlist
                for(let tier_index of Object.keys(tierlist.tiers)){
                    if(!tierlist.tiers[tier_index].includes(id)) continue

                    let anime_index = tierlist.tiers[tier_index].indexOf(id)
                    let element = `<li id="${id}" class="">${anime.outerHTML.replace('167', '80')}</li>`

                    tiersOrdered[tier_index] ??= []
                    tiersOrdered[tier_index][anime_index] = element
                }
            }else{ // add shows to last tier by default
                document.querySelector('#last').innerHTML += `<li id="${id}" class="">${anime.outerHTML.replace('167', '80')}</li>`
            }
        }

        // insert anime to tierlist
        if(tierlist && tierlist.version == 2){
            for(let tier_index of Object.keys(tierlist.tiers)){
                let tier_row = document.getElementsByClassName('grid-container')[0].querySelectorAll('ul.droptrue')[tier_index]
                tier_row.innerHTML = tiersOrdered[tier_index].join('\n')
            }
        }

        // tier label names
        let tier_names = tierlist?.tier_names ?? JSON.parse(localStorage.getItem('tier_names'+season))
        if(tier_names){
            for(let [i, textareas] of document.querySelectorAll('ul div.textarea').entries()){
                textareas.innerText = tier_names[i]
            }
            // document.querySelectorAll('ul div.textarea').entries().forEach((i, textareas) => { textareas.innerText = tier_names[i] })
        }

        document.querySelector('#menu').scrollIntoView(); // window.scrollTo(0, 50);

        // automatic save when making tierlist
        // https://stackoverflow.com/questions/3219758/detect-changes-in-the-dom/3219767#3219767
        var targetNode = document.getElementsByClassName('grid-container')[0];
        var config = { subtree: true, childList: true, characterData: true,}; // works with this config

        var observer = new MutationObserver(SaveTierlistV2);
        observer.observe(targetNode, config); // detect dom change when creating tierlist

        // delete button event
        document.querySelector('#deletetierlist').onclick = function(event){
            event.preventDefault(); // disable default link click

            if (confirm('Delete tierlist from localstorage?')) {
                let season = document.querySelector("#content .season_nav .on").textContent
                localStorage.removeItem(season);
                localStorage.removeItem('tier_names'+season);
                RenderTierlist();
            }
        };
    }

    function SaveTierlist(){
        let season = document.querySelector("#content .season_nav .on").textContent
        let tierlist = {}
        for(let tier of document.getElementsByClassName('grid-container')[0].querySelectorAll('ul.droptrue')){
            for(let anime of tier.querySelectorAll('li')){
                tierlist[anime.id] = tier.id
            }
        }
        localStorage.setItem(season, JSON.stringify(tierlist));

        let tier_names = Array.from(document.querySelectorAll('ul div.textarea')).map(tier => tier.textContent)
        localStorage.setItem('tier_names'+season, JSON.stringify(tier_names));
    }

    function SaveTierlistV2(){
        let season = document.querySelector("#content .season_nav .on").textContent
        let tierlist = {version:2}

        tierlist.tiers = Array.from(document.querySelectorAll('.grid-container')[0].querySelectorAll('ul.droptrue')).map(tier => Array.from(tier.querySelectorAll('li')).map(anime => anime.id))
        tierlist.tier_names = Array.from(document.querySelectorAll('ul div.textarea')).map(tier => tier.textContent)

        localStorage.setItem(season, JSON.stringify(tierlist));
    }

    // anime count per season
    let seasonal = document.querySelectorAll('.seasonal-anime-list:nth-child(1) .seasonal-anime'),
        count = 0,
        cours = 0
    Array.from(seasonal).forEach(anime => {
        let [episodes, duration] = Array.from(anime.querySelectorAll('.info .item:nth-child(2) span')).map(info => parseInt(info.textContent) ?? 0)
        let members = anime.querySelector('.member').textContent
            members = members.match(/[\d.]+/) * ( members.match(/M/) ? 1000000 : ( members.match(/K/) ? 1000 : 1 ) )
        if(duration > 15 || members > 2000) count++
        if(episodes > 10) cours += episodes/13
    })
    document.querySelectorAll('.anime-header')[0].innerText += ` (${count})`//+` (${cours.toFixed(0)})`

    // buggy, zooms images
    // https://www.geeksforgeeks.org/how-to-take-screenshot-of-a-div-using-javascript/
    // download image button
    //     document.getElementById('download').addEventListener("click", function() {
    //         function download(canvas, filename) {
    //             const data = canvas.toDataURL("image/png;base64");
    //             const donwloadLink = document.querySelector("#download");
    //             donwloadLink.download = filename;
    //             donwloadLink.href = data;
    //         }

    //        html2canvas(document.querySelector(".grid-container"), {
    //            useCORS: true,
    //            onrendered: function (canvas) {
    //                document.body.appendChild(canvas);
    //            }}).then((canvas) => {
    //            // document.body.appendChild(canvas);

    //            download(canvas, "seasonal tierlist");
    //        });
    //     });

})();