WaniKani Dashboard Leech Tables - Notice your leeches

Shows your Leeches on the Wanikani dashboard

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();