Ky.is WaniKani JS Replacement

Replaces Javascript on ky.is/wanikani to support WaniKani updates.

目前為 2015-07-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Ky.is WaniKani JS Replacement
// @namespace   rfindley
// @description Replaces Javascript on ky.is/wanikani to support WaniKani updates.
// @include     http://ky.is/wanikani/*
// @version     1.0.0
// @author      Robin Findley
// @copyright   2015+, Robin Findley
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-start
// @grant       none
// ==/UserScript==

/*
//=========================================================================================================

The following code block fetches the number of kanji in each level on WaniKani.
Paste the code in the javascript console while on the WaniKani Dashboard.
It should output something like this:

  new_KANJI_LEVEL_SIZES = [18,38,33,38,42,40,33,32,35,35,38,37,37,32,33,35,33,29,34,32,32,31,31,31,33,33,32,34,33,31,36,33,32,34,32,33,32,32,34,32,30,33,35,34,35,37,36,37,33,35,35,35,35,35,35,35,35,35,35,32];

//--[ begin ]---------------------------------
var level_hrefs=[];
$('.nav .levels li a').each(function(i,e){level_hrefs.push($(e).attr('href'));});
var num_levels = level_hrefs.length;

var num_kanji = [];
$(level_hrefs).each(function(i,href){
  num_kanji[i] = 0;
  $.get(href,function(data){
    num_kanji[i] = $(data).find('.single-character-grid li[id^="kanji"]').length;
  });
});

console.log('new_KANJI_LEVEL_SIZES = ['+num_kanji+'];');
//--[ end ]---------------------------------

//=========================================================================================================
*/

new_KANJI_LEVEL_SIZES = [18,38,33,38,42,40,33,32,35,35,38,37,37,32,33,35,33,29,34,32,32,31,31,31,33,33,32,34,33,31,36,33,32,34,32,33,32,32,34,32,30,33,35,34,35,37,36,37,33,35,35,35,35,35,35,35,35,35,35,32];

var new_stats = [
    ['Grade 1', 8],
    ['Grade 2', 18],
    ['JLPT4', 27],
    ['', 40],
    ['', 50],
    ['complete!', 60]
];

var number_of_standard_deviations = 2.0;

function DayDateAt(sec){
    sec = Math.ceil(sec / (15*60)) * (15*60);
    var t = new Date(sec*1000)
    var wday = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][t.getDay()];
    var hh = ((t.getHours()+23) % 12)+1;
    var mm = t.getMinutes(); if (mm<10) mm = "0"+mm;
    var ampm = (t.getHours()<12 ?'am':'pm')
    return '('+wday+' '+hh+':'+mm+ampm+')';
}

function daysHours(t) {
    return '['+(Math.floor(Math.round(t/3600)/24))+' days, '+(Math.round(t/3600)%24)+' hours]';
}

function new_statsCallback(data, status) {
	if (!callbackCheck(data))
		return;
	var radicals_data = data.requested_information;
	if (!radicals_data) {
		alert('Unable to load your WaniKani API data');
		return;
	}
	var user_data = data.user_information;
	currentLevel = user_data.level;

	$('#stats-data').removeClass('hidden');

	var currentTime = Math.round(new Date().getTime() / 1000);
	var levelStats = new Array(currentLevel);
	for (var idx in radicals_data) {
		var rd = radicals_data[idx];
		var stats = rd.user_specific;
		if (stats) {
			var lvlIdx = rd.level - 1;
			var cbt = levelStats[lvlIdx];
			var timing = stats.unlocked_date;
			if (!cbt || timing < cbt)
				levelStats[lvlIdx] = timing;
		}
	}
	if (!levelStats[currentLevel-1])
		levelStats[currentLevel-1] = currentTime;

	var currentLevelTime;
	levelStats[currentLevel] = currentTime;
	lastLevelUp = levelStats[currentLevel - 1];
	currentLevelTime = currentTime - levelStats[currentLevel - 1];

	var timePerLevel = new Array(currentLevel - 1);
	var levelTimes = new Array(currentLevel);
	var longestLevelTime = 0, fastestLevelTime = Number.MAX_VALUE;
	for (var i=0; i<currentLevel; ++i) {
		var diff = levelStats[i+1] - levelStats[i];
		levelTimes[i] = diff;
		if (diff > longestLevelTime)
			longestLevelTime = diff;
		if (i < currentLevel - 1) {
			timePerLevel[i] = diff;
			if (diff < fastestLevelTime)
				fastestLevelTime = diff;
		}
	}
	longestLevelTime /= T2DAYS;
	fastestLevelTime /= T2DAYS;
	var maxDays = Math.ceil(longestLevelTime / 3.0) * 3;
	var dayInterval = maxDays / 3;

	$('form#stats-form').hide();
	$('#st-user').html(username + ' <span lang="ja" class="note">' + numberToKanji(currentLevel) + '</span>');
	var start_date = new Date(user_data.creation_date * 1000);
	var nowTime = new Date().getTime();
	var dayDiff = Math.round(((nowTime / 1000) - user_data.creation_date) / 3600 / 24);
	$('#st-date').html('Worshiping the Crabigator since ' + start_date.getDate() + ' ' + MONTH_NAMES[start_date.getMonth()] + ' ' + start_date.getFullYear() + ' <em>(' + dayDiff + ' days)</em>');

/* 	Average time */
    var avgTime = 0;
    var reject = [];
    var stda = [];
    var median, medianDev;
    if (currentLevel > 1) {
        var sortedTimePerLevel = timePerLevel.slice(0);
        var sortedDevPerLevel = [];
        sortedTimePerLevel.sort(numericalSort);
        median = sortedTimePerLevel[Math.floor((currentLevel-1)/2)];
        for (var i = 0; i < currentLevel - 1; ++i)
            sortedDevPerLevel.push(Math.abs(timePerLevel[i]-median));
        sortedDevPerLevel.sort(numericalSort);
        medianDev = sortedDevPerLevel[Math.floor((currentLevel-1)*(currentLevel<8?0.5:0.75))];

        var min = median-(medianDev*number_of_standard_deviations);
        var max = median+(medianDev*number_of_standard_deviations);
        var sum=0, cnt=0;
        for (var i = 0; i < currentLevel - 1; ++i) {
            if (Math.abs(timePerLevel[i]-median) <= (medianDev*number_of_standard_deviations)) {
                sum += timePerLevel[i];
                cnt++;
            } else {
                reject.push(i+1);
            }
        }
        avgTime = sum / cnt;
    }
    if (avgTime < 8 * T2DAYS)
        avgTime = 8 * T2DAYS;
    averageLevelTime = avgTime;
    console.log('Using Median '+daysHours(median)+' +/- '+daysHours(medianDev*number_of_standard_deviations)+'.');
    console.log('Averaging levels between '+daysHours(min)+' and '+daysHours(max)+'.');
    console.log('Rejecting levels ['+reject.join()+']');
	currLevelDiff = levelTimes[levelTimes.length - 1];
	var curr_level_fraction = currLevelDiff < avgTime ? currentLevel + currLevelDiff / avgTime : currentLevel + 1;

    var dates = [];
    var labels = [];
    var completion = [];
    for (var i=0; i<new_stats.length; i++)
    {
        dates.push(new Date(nowTime + avgTime * ((new_stats[i][1]+1) - curr_level_fraction) * 1000));
        labels.push('<span class="note">'+new_stats[i][0]+'</span> Level '+('0'+new_stats[i][1]).slice(-2)+':');
        completion.push('<span>'+dateFormat(dates[i])+'</span>');
    }
    labels = labels.join('<br />');
    completion = completion.join('<br />');
	avgTime /= T2DAYS;
	$('#stat-average').html(dayDate(avgTime));
	$('#stat-leveltime').html(dayDate(currentLevelTime / T2DAYS));
    $('#info-container .info-box:nth-child(2) .info-left').html(labels).css('vertical-align','top');
	$('#stat-completion').html(completion).parent().css('vertical-align','top');

	var canvas = document.getElementById('levelgraph');
	if (canvas.getContext) {
		var barW = 96 - (currentLevel * 5);
		if (barW < 30)
			barW = 30;
		canvas.width = currentLevel * (barW + BS) - BS + 40;
		canvas.height = GH;
		var ctx = canvas.getContext('2d');
		ctx.textBaseline = 'top';
		ctx.font = '12px sans-serif';
		for (var i = 0; i < 4; ++i) {
			ctx.fillText(maxDays - i * dayInterval, 2, i * BA / 3);
		}
		ctx.textAlign = 'center';
		ctx.font = '14px sans-serif';
		for (var i=0; i<currentLevel; ++i) {
			var levelDays = levelTimes[i] / T2DAYS;
			var color = i == currentLevel-1 ? 'pink' : levelDays - fastestLevelTime < 0.1 ? 'baby' : longestLevelTime - levelDays < 0.1 ? 'black' : levelDays < avgTime ? 'blue' : 'purp';
			drawBar(ctx, i, levelDays, color, barW, levelDays / maxDays * BA);
		}
	}

/* 	Levelup */
	getApiData(stats_key, 'kanji/' + currentLevel, 'levelupCallback');
}

function new_levelupCallback(data, status) {
	if (!callbackCheck(data))
		return;
	var kanji_data = data.requested_information;
	kanji_data.sort(function(a, b) {
		var aStats = a.user_specific;
		if (!aStats)
			return 1;
		var bStats = b.user_specific;
		if (!bStats)
			return -1;
		if (aStats.srs !== "apprentice")
			return -1;
		if (bStats.srs !== "apprentice")
			return 1;

		var aMax = Math.min(aStats.meaning_current_streak, aStats.reading_current_streak);
		var bMax = Math.min(bStats.meaning_current_streak, bStats.reading_current_streak);
		if (aMax < bMax)
			return 1;
		if (aMax > bMax)
			return -1;
		return aStats.available_date - bStats.available_date;
	});
	var idx90 = Math.ceil(kanji_data.length * 0.9);
	var kanji90 = kanji_data[idx90 - 1];
	var statsDict = kanji90.user_specific;
	var nextLevelUp;
	var cTime = Math.round(new Date().getTime() / 1000);
	if (!statsDict) {
		var half_level = 4.0 * T2DAYS;
		nextLevelUp = lastLevelUp + (averageLevelTime < 8 * T2DAYS ? half_level * 2.0 : averageLevelTime) - cTime;
		if (nextLevelUp < half_level)
			nextLevelUp = half_level;
	} else {
		var streak = Math.min(statsDict.meaning_current_streak, statsDict.reading_current_streak);
		nextLevelUp = statsDict.available_date - cTime;
		if (streak < 3) {
			nextLevelUp += 3.0 * T2DAYS;
			if (streak < 2) {
				nextLevelUp += 1.0 * T2DAYS;
				if (streak == 0)
					nextLevelUp += 0.17 * T2DAYS;
			}
		}
	}
	$('#stat-levelup').html(dayDate(nextLevelUp / T2DAYS));
    $('#stat-levelupat').html(DayDateAt(nextLevelUp+cTime));
}

function new_main()
{
    KANJI_LEVEL_SIZES = new_KANJI_LEVEL_SIZES;
    window.statsCallback = new_statsCallback;
    window.levelupCallback = new_levelupCallback;

    var labels = $('#info-container .info-left:nth(0)');
    labels.html(labels.html().replace('Level Up In:','Level Up In:<br>'));
    var stats = $('#stat-levelup');
    stats.after('<br><span id="stat-levelupat">Loading...');
}

var old_log = console.log;
console.log = function(text) {
    if (text == 'Requesting stats information') {
        console.log = old_log;
        console.log(text);
        new_main();
    } else {
        old_log.apply(old_log,arguments);
    }
}