WaniKani Dashboard Leech Tables - Notice your leeches

Shows your Leeches on the Wanikani dashboard

// ==UserScript==
// @name          WaniKani Dashboard Leech Tables - Notice your leeches
// @namespace     wk-dashboard-leech-display
// @description   Shows your Leeches on the Wanikani dashboard
// @author        Dani2
// @version       1.6
// @include       https://www.wanikani.com/dashboard
// @include       https://www.wanikani.com/
// @grant         none
// ==/UserScript==

(function() {
    'use strict';
    //------------------------------
	// Wanikani Framework
	//------------------------------
	if (!window.wkof) {
		let response = confirm('WaniKani Dashboard Leech List script requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');

		if (response) {
			window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
		}

		return;
	}

	const config = {
		wk_items: {
			options: {
				review_statistics: true
			},
            filters: {
                level: '1..+0', //only retrieve items from lv 1 up to and including current level
                srs: {value: 'lock, init, burn', invert: true} //exlude locked, initial and burned items
            }
		}
	};

	wkof.include('ItemData, Menu, Settings');
	wkof.ready('ItemData, Menu, Settings').then(install_menu).then(install_settings).then(getItems).then(getLeechScores).then(updatePage).then(toggleExistingItemsTables);

    //------------------------------
	// Styling
	//------------------------------
    var leechTableCss = `
        .noLeeches {
            margin: 0 0 20px 0;
        }
        .radicalCharacterImgSize {
            font-size: 12px;
        }
        /*TOOLTIP*/

        [tooltip]:hover:before {
            /* needed - do not touch */
            opacity: 1;

            /* customizable */
            background: yellow;
            margin-top: -50px;
            margin-left: 20px;
        }

        [tooltip]:not([tooltip-persistent]):before {
            pointer-events: none;
        }

        a.tooltipImg strong {line-height:30px;}
        a.tooltipImg span {
            z-index:10;display:none; padding:7px 10px;
            margin-top:30px; margin-left:-160px;
            width:300px; line-height:16px;
        }
        a.tooltipImg:hover span{
            display:inline; position:absolute;
            border:2px solid #FFF;  color:#EEE;
            background:#333 url(https://cdn.wanikani.com/default-avatar-300x300-20121121.png) repeat-x 0 0;
        }

        .callout {z-index:20;position:absolute;border:0;top:-14px;left:120px;}

        a.tooltipImg span
        {
            border-radius:2px;
            box-shadow: 0px 0px 8px 4px #666;
            /*opacity: 0.8;*/
        }
        a.tooltipImg:before {
            pointer-events: none;
        }
        /*END TOOLTIP*/

        /*makes space*/
        #leech-table:after{
            clear: none;
        }
        #leaderboard:after{
            clear: none;
        }`;

    var leechStyling = document.createElement('style');
    leechStyling.type='text/css';
    if(leechStyling.styleSheet){
        leechStyling.styleSheet.cssText = leechTableCss;
    }else{
        leechStyling.appendChild(document.createTextNode(leechTableCss));
    }
    document.getElementsByTagName('head')[0].appendChild(leechStyling);

    //------------------------------
	// Menu
	//------------------------------
    var settings_dialog;
    var defaults = {
        totalNumberOfLeeches: 30,
        leechesPerTable: 10,
        leechThreshold: 1,
        newlyUnlockedItems: false,
        criticalConditionsItems: false,
        newlyBurnedItems: false,
    };

    function install_menu() {
        wkof.Menu.insert_script_link({
            script_id: 'Leech_Tables',
            name: 'Leech_Tables',
            submenu:   'Settings',
            title:     'Leech Tables',
            on_click:  open_settings
        });
    }

    function open_settings() {
        settings_dialog.open();
    }

    function install_settings() {
        settings_dialog = new wkof.Settings({
            script_id: 'Leech_Tables',
            name: 'Leech_Tables',
            title: 'Leech Tables',
            on_save: process_settings,
            settings: {
                'totalNumberOfLeeches': {type:'dropdown',label:'Total number of leeches',hover_tip: 'The amount of leeches you want to display',default:defaults.totalNumberOfLeeches,
                                         content:{5:'5 (Min)', 10:'10', 15:'15', 20:'20', 25:'25', 30:'30 (default)', 35:'35', 40:'40', 45:'45', 50:'50', 100:'100', 200:'200'}},
                'leechesPerTable': {type:'dropdown',label:'Number of leeches per table',hover_tip: 'Defines how many leeches will be included per table',default:defaults.leechesPerTable,
                                    content:{10:'10 (default)', 15:'15', 20:'20', 25:'25', 30:'30', 35:'35', 40:'40'}},
                'leechThreshold': {type:'dropdown',label:'Leech threshold',hover_tip: 'Determine yourself what is and isn\'t a leech',default:defaults.leechThreshold,
                                   content:{1:'1 (default)', 2:'2', 3:'3', 4:'4', 5:'5', 6:'6', 7:'7', 8:'8', 9:'9', 10:'10', 100:'100'}},
                'newlyUnlockedItems': {type:'checkbox',hover_tip: 'If selected removes the \'newly unlocked items\' table',label:'Disable newly unlocked items',default:defaults.newlyUnlockedItems},
                'criticalConditionsItems': {type:'checkbox',hover_tip: 'If selected removes the \'critical conditions items\' table',label:'Disable critical conditions items',default:defaults.criticalConditionsItems},
                'newlyBurnedItems': {type:'checkbox',hover_tip: 'If selected removes the \'newly burned items\' table',label:'Disable newly burned items',default:defaults.newlyBurnedItems}
            }
        });
        settings_dialog.load().then(function(){
            wkof.settings.Leech_Tables = $.extend(true, {}, defaults,wkof.settings.Leech_Tables);
            settings_dialog.save();
        });
    }
    function process_settings(){
        settings_dialog.save();
        //refresh leech tables
        getItems().then(getLeechScores).then(updatePage);
        toggleExistingItemsTables();
    }

    //------------------------------
	// Leech List
	//------------------------------
	function getItems() {
		return wkof.ItemData.get_items(config);
	}

	function getLeechScores(items) {
		return items.filter(isLeech);
	}

	function isLeech(item) {
		if (item.review_statistics === undefined) {
			return false;
		}

		let reviewStats = item.review_statistics;
		let meaningScore = getLeechScore(reviewStats.meaning_incorrect, reviewStats.meaning_current_streak);
		let readingScore = getLeechScore(reviewStats.reading_incorrect, reviewStats.reading_current_streak);

        item.leech_score = Math.max(meaningScore, readingScore);
		return item.leech_score >= parseFloat(wkof.settings.Leech_Tables.leechThreshold);
	}

	function getLeechScore(incorrect, currentStreak) {
        //get incorrect number than lessen it using the user's correctStreak
        let leechScore = incorrect / Math.pow((currentStreak || 0.5), 1.5); // '||' => if currentstreak zero make 0.5 instead (prevents dividing by zero)
        leechScore = Math.round(leechScore * 100) / 100; //round to two decimals
		return leechScore;
	}

	function updatePage(items) {
        //sort by leechscore, if score equal sort by level ascending
        items = items.sort(function(a, b){return (a.leech_score == b.leech_score) ? a.data.level - b.data.level : b.leech_score - a.leech_score}).slice(0, parseInt(wkof.settings.Leech_Tables.totalNumberOfLeeches));

        createTopLeechTables(items);
	}

    function itemsCharacterCallback (itemsData){
        //check if an item has characters. Kanji and vocabulary will always have these but wk-specific radicals (e.g. gun, leaf, stick) use images instead
        if(itemsData.characters!= null) {
            return itemsData.characters;
        } else if (itemsData.character_images!= null){
            return '<i class="radical-'+itemsData.slug+' radicalCharacterImgSize"></i>';
            //return '<img class="radical-img" src="https://cdn.wanikani.com/subjects/images/8786-worm-small.png">'
        } else {
            //if both characters and character_images are somehow absent try using slug instead
            return itemsData.slug;
        }
    }

    function createTopLeechTables(items) {
        let sectionContents = "";
        let numberPerTable = parseInt(wkof.settings.Leech_Tables.leechesPerTable);
        let startnumberTable = 0;
        let endNumberTable = numberPerTable;
        let nrOfTables = 3;
        let noLeechesFoundMessage = 'There are no leeches available. Have you tried lowering the leechthreshold?';
        let noLeechesFoundStyling = '';

        //make sure we don't create empty tables if there are too few leeches
        if(items.length == 0){ //if no leeches
            nrOfTables = 0;
            //if threshold already at the lowest then change message to no longer request they lower it
            if(wkof.settings.Leech_Tables.leechThreshold <= 1){
                noLeechesFoundMessage = 'You have no leeches at the moment.';
            }
            noLeechesFoundStyling = 'alert fade in noLeeches';
            sectionContents += noLeechesFoundMessage;
        } else if(items.length < wkof.settings.Leech_Tables.totalNumberOfLeeches) { //if less leeches available then user requested
            var ratio = items.length / (numberPerTable*3);
            if(ratio <= 0.34){
                nrOfTables = 1;
            } else if(ratio <= 0.67){
                nrOfTables = 2;
            }
        } else if (numberPerTable >= wkof.settings.Leech_Tables.totalNumberOfLeeches){ //if table capacity greater than user's requested amount of leeches
            nrOfTables = 1;
        }

        //Create leech tables
        for (var i = 0; i < nrOfTables; i++){
            //In case there are less than the requested amount of leeches
            if(items.length <= endNumberTable){
                endNumberTable = items.length; nrOfTables--;
            }
            sectionContents += `
                <div class="span4">
                    <section class="kotoba-table-list dashboard-sub-section" style="position: relative;">
                        <h3 class="small-caps">Top Leeches ${startnumberTable+1}-${endNumberTable}</h3>
                            <table>
                                <tbody>`;
            for (var j = startnumberTable; j < endNumberTable; j++){
                sectionContents += `<tr class="${items[j].object}">
                                        <td tooltip="${items[j].data.meanings[0].meaning} ${items[j].data.readings !== undefined ? ', '+items[j].data.readings[0].reading : ""}">
                                            <a href="${items[j].data.document_url}"><span lang="ja">${itemsCharacterCallback(items[j].data)}</span><span class="pull-right">${items[j].leech_score}</span></a>
                                        </td>
                                    </tr>`;
            }
            //preparing for next table
            startnumberTable += numberPerTable;
            endNumberTable += numberPerTable;

            sectionContents += `
                                </tbody>
                            </table>
                    </section>
                </div>`;
        }

        let leechTableStyle = '<div id="leech_table" class="row '+noLeechesFoundStyling+'">'
        sectionContents += `</div>`;

        //check if a leech table is already there
        if(document.getElementById("leech_table")) {
            sectionContents = leechTableStyle + sectionContents;
            $('#leech_table ').replaceWith(sectionContents);//replace existing list
        } else {
            if ($('section.progression').length) {
                $('section.progression').after(leechTableStyle);
            }
            else {
                $('section.srs-progress').after(leechTableStyle);
            }
            $('#leech_table ').append(sectionContents);
        }

        //eventlisteners
        //document.getElementById('leech_table-stats').addEventListener('click', test);
    }

    //------------------------------
	// New Unlocks, Critical Condition and Newly Burned Items
	//------------------------------
    function toggleExistingItemsTables(){
        let newlyUnlockedItemTable = document.querySelector(".span4 > .recent-unlocks");
        let criticalItemTable = document.querySelector(".span4 > .low-percentage");
        let newlyBurnedItemTable = document.querySelector(".span4 > .recent-retired");

        if(wkof.settings.Leech_Tables.newlyUnlockedItems){
            newlyUnlockedItemTable.closest("div").style.display = "none";
        }else {
            newlyUnlockedItemTable.closest("div").style.display = "block";
        }
        if(wkof.settings.Leech_Tables.criticalConditionsItems){
            criticalItemTable.closest("div").style.display = "none";
        }else {
            criticalItemTable.closest("div").style.display = "block";
        }
        if(wkof.settings.Leech_Tables.newlyBurnedItems){
            newlyBurnedItemTable.closest("div").style.display = "none";
        }else {
            newlyBurnedItemTable.closest("div").style.display = "block";
        }
    }

})();