// ==UserScript==
// @name WaniKani Leech Trainer
// @version 1.0.3
// @description Study and quiz yourself on your leeches!
// @require https://greasyfork.org/scripts/19781-wanakana/code/WanaKana.js?version=126349
// @author hitechbunny
// @author pamput
// @include https://www.wanikani.com/
// @include https://www.wanikani.com/dashboard
// @run-at document-end
// @grant none
// @namespace https://greasyfork.org/users/149329
// ==/UserScript==
(function() {
'use strict';
// Hook into App Store
try { $('.app-store-menu-item').remove(); $('<li class="app-store-menu-item"><a href="https://community.wanikani.com/t/there-are-so-many-user-scripts-now-that-discovering-them-is-hard/20709">App Store</a></li>').insertBefore($('.navbar .dropdown-menu .nav-header:contains("Account")')); window.appStoreRegistry = window.appStoreRegistry || {}; window.appStoreRegistry[GM_info.script.uuid] = GM_info; localStorage.appStoreRegistry = JSON.stringify(appStoreRegistry); } catch (e) {}
var css =
'.noselect {-webkit-touch-callout:none; -webkit-user-select:none; -khtml-user-select:none; -moz-user-select: none;'+
'-ms-user-select:none; user-select: none;}'+
'.selfstudy {margin-left:20px; margin-bottom:10px; position:relative;}'+
'.selfstudy label {display:inline; vertical-align:middle; padding-right:4px; color:#999; font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif; text-shadow:0 1px 0 #fff;}'+
'.selfstudy button.enable {width:55px;}'+
'.ss_active .selfstudy button.enable.on {background-color:#b3e6b3; background-image:linear-gradient(to bottom, #ecf9ec, #b3e6b3);}'+
'.selfstudy select.config {width:300px;}'+
'.selfstudy .center {display:block; position:relative; top:50%; left:50%; transform:translate(-50%,-50%);}'+
'section[id^="level-"].ss_active.ss_hidechar .character-item a span:not(.dummy) {opacity:0; transition:opacity ease-in-out 0.15s}'+
'section[id^="level-"].ss_active.ss_hideread .character-item a li[lang="ja"] {opacity:0; transition:opacity ease-in-out 0.15s}'+
'section[id^="level-"].ss_active.ss_hidemean .character-item a li:not([lang="ja"]) {opacity:0; transition:opacity ease-in-out 0.15s}'+
'section[id^="level-"].ss_active.ss_hideburned .character-item.burned {display:none;}'+
'section[id^="level-"].ss_active.ss_hidelocked .character-item.locked {display:none;}'+
'section[id^="level-"].ss_active.ss_hideunburned .character-item:not(.burned) {display:none;}'+
'section[id^="level-"].ss_active.ss_hideunlocked .character-item:not(.locked) {display:none;}'+
'section.ss_active .character-item:hover a span {opacity: initial !important; transition:opacity ease-in-out 0.05s !important;}'+
'section.ss_active .character-item:hover a li {opacity: initial !important; transition:opacity ease-in-out 0.05s !important;}'+
'#ss_config {position:absolute; z-index:1029; width:573px; background-color:rgba(0,0,0,0.9); border-radius:8px; padding:8px;}'+
'#ss_config select.configs {width:475px;}'+
'#ss_config label {color:#ccc; text-shadow:initial; text-align:right; vertical-align:baseline;}'+
'#ss_config .btns {display:inline-block; float:left; vertical-align:top; margin-right:8px;}'+
'#ss_config .btns .btn {display:block; margin-bottom:5px;}'+
'#ss_config .btn {width:70px;}'+
'#ss_config .list {overflow-x:auto;}'+
'#ss_config .list select.configs {width:100%; height:135px;}'+
'#ss_config .section {border-top:1px solid #ccc; padding:0 0 8px 0;}'+
'#ss_config .section > label {display:block; text-align:left; color:#ffc; font-size:1.2em; font-weight:bold; padding-left:4px; margin-bottom:4px; background-color:#2e2e2e; background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;}'+
'#ss_config .txtline label {display:inline-block; float:left; margin-right:8px; width:100px; line-height:30px; clear:both;}'+
'#ss_config .txtline .expand {overflow-x:auto;}'+
'#ss_config .txtline input {box-sizing:border-box; width:100%; height:30px;}'+
'#ss_config .cbbox {display:inline-block; width:49%; vertical-align:top;}'+
'#ss_config .cbbox label {display:inline-block; float:left; margin:0 8px 0 0; width:190px; line-height:20px;}'+
'#ss_config .cbbox input {position:relative; overflow-x:auto; height:20px; margin:0; top:1px;}'+
'#ss_config [class*="icon-"] {color:#fff;}'+
'#ss_config .dlg_close {text-align:center; margin-top:16px; margin-bottom:8px;}'+
'#ss_quiz [lang="ja"] {font-family: "Meiryo","Yu Gothic","Hiragino Kaku Gothic Pro","TakaoPGothic","Yu Gothic","ヒラギノ角ゴ Pro W3","メイリオ","Osaka","MS PGothic","MS Pゴシック",sans-serif;}'+
'#ss_quiz {position:absolute; z-index:1028; width:573px; background-color:rgba(0,0,0,0.85); border-radius:8px; border:8px solid rgba(0,0,0,0.85); font-size:2em;}'+
'#ss_quiz * {text-align:center;}'+
'#ss_quiz .qwrap {height:8em; position:relative; clear:both;}'+
'#ss_quiz.radicals .qwrap, #ss_quiz.radicals .summary .que {background-color:#0af;}'+
'#ss_quiz.kanji .qwrap, #ss_quiz.kanji .summary .que {background-color:#f0a;}'+
'#ss_quiz.vocabulary .qwrap, #ss_quiz.vocabulary .summary .que {background-color:#a0f;}'+
'#ss_quiz .prev, #ss_quiz .next {display:inline-block; width:80px; color:#fff; line-height:8em; cursor:pointer;}'+
'#ss_quiz .prev:hover {background-image:linear-gradient(to left, rgba(0,0,0,0), rgba(0,0,0,0.2));}'+
'#ss_quiz .next:hover {background-image:linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.2));}'+
'#ss_quiz .prev {float:left;}'+
'#ss_quiz .next {float:right;}'+
'#ss_quiz .topbar {font-size:0.5em; line-height:1em; color: rgba(255,255,255,0.5);}'+
'#ss_quiz .settings {float:left; padding:6px 8px; text-align:left; line-height:1.5em;}'+
'#ss_quiz .settings span[class*="icon-"] {font-size:1.3em; padding:0 2px;}'+
'#ss_quiz .settings .ss_audio {padding-left:0; padding-right:4px;}'+
'#ss_quiz .settings .ss_typo {padding-left:0px;}'+
'#ss_quiz .settings .ss_done {font-size:1.25em;}'+
'#ss_quiz .settings .ss_pair {font-weight:bold;}'+
'#ss_quiz .settings span {cursor:pointer;}'+
'#ss_quiz .settings span:hover {color:rgba(255,255,204,0.8);}'+
'#ss_quiz .settings span.active {color:#ffc;}'+
'#ss_quiz.help .settings .ss_help {color:#ffc;}'+
'#ss_quiz .stats_labels {text-align:right; font-family:monospace;}'+
'#ss_quiz .stats {float:right; text-align:right; color:rgba(255,255,255,0.8); font-family:monospace; padding:0 5px;}'+
'#ss_quiz .round {display:none; font-weight:bold; position:absolute; box-sizing:border-box; width:60%; height:75%; border-radius:24px; border:2px solid #000; background-color:#fff;}'+
'#ss_quiz.round .round {display:block;}'+
'#ss_quiz .question {'+
' overflow-x:auto; overflow-y:hidden; position:relative; top:50%; transform:translateY(-50%);'+
' color:#fff; text-align:center; line-height:1.1em; font-size:1em; font-weight:bold; cursor:default;'+
'}'+
'#ss_quiz .question[data-type="char"] {font-size:2em;}'+
'#ss_quiz .icon-audio:before {content:"\\f028";}'+
'#ss_quiz .question .icon-audio {font-size:2.5em; cursor:pointer;}'+
'#ss_quiz.summary .question {display:none;}'+
'#ss_quiz .qtype {line-height:2em; cursor:default; text-transform:capitalize;}'+
'#ss_quiz .qtype.reading {color:#fff; text-shadow:-1px -1px 0 #000; border-top:1px solid #555; border-bottom:1px solid #000; background-color:#2e2e2e; background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;}'+
'#ss_quiz .qtype.meaning {color:#555; text-shadow:-1px -1px 0 rgba(255,255,255,0.1); border-top:1px solid #d5d5d5; border-bottom:1px solid #c8c8c8; background-color:#e9e9e9; background-image:linear-gradient(to bottom, #eee, #e1e1e1); background-repeat:repeat-x;}'+
'#ss_quiz .help {display:none;'+
' position:absolute; top:3%; left:13%; width:74%; box-sizing:border-box; border:2px solid #000; border-radius:15px; padding:4px;'+
' color:#555; text-shadow:2px 2px 0 rgba(0,0,0,0.2); background-color:rgba(255,255,255,0.9); font-size:0.8em; line-height:1.2em;'+
'}'+
'#ss_quiz.help .help {display:inherit;}'+
'#ss_quiz .answer {background-color:#ddd; padding:8px;}'+
'#ss_quiz .answer input {'+
' width:100%; background-color:#fff; height:2em; margin:0; border:2px solid #000; padding:0;'+
' box-sizing:border-box; border-radius:0; font-size:1em;'+
'}'+
'#ss_quiz .answer input.correct {color:#fff; background-color:#8c8; text-shadow:2px 2px 0 rgba(0,0,0,0.2);}'+
'#ss_quiz .answer input.incorrect {color:#fff; background-color:#f03; text-shadow:2px 2px 0 rgba(0,0,0,0.2);}'+
'#ss_quiz.loading .qwrap, #ss_quiz.loading .answer {display:none;}'+
'#ss_quiz .summary {display:none; position:absolute; width:74%; height:100%; background-color:rgba(0,0,0,0.7); color:#fff; font-weight:bold;}'+
'#ss_quiz.summary .summary {display:block;}'+
'#ss_quiz .summary h3 {'+
' background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;'+
' border-top:1px solid #777; border-bottom:1px solid #000; margin:0; box-sizing:border-box;'+
' text-shadow:2px 2px 0 rgba(0,0,0,0.5); color:#fff; font-size:0.8em; font-weight:bold; line-height:40px;'+
'}'+
'#ss_quiz .summary .errors {position:absolute; top:40px; bottom:0px; width:100%; margin:0; overflow-y:auto; list-style-type:none;}'+
'#ss_quiz .summary li {margin:4px 0 0 0; font-size:0.6em; font-weight:bold; line-height:1.4em;}'+
'#ss_quiz .summary .errors span {display:inline-block; padding:2px 4px 0px 4px; border-radius:4px; line-height:1.1em; max-width:50%; vertical-align:middle; cursor:pointer;}'+
'#ss_quiz .summary .ans {background-color:#fff; color:#000;}'+
'#ss_quiz .summary .wrong {color:#f22;}'+
'#ss_quiz .btn.requiz {position:absolute; top:6px; right:6px; padding-left:6px; padding-right:6px;}'+
'#ss_quiz_container {position:absolute; top:0; left:0; width:100%}'+
'#ss_quiz {position: fixed; margin-left: auto; margin-right: auto; left: 0; right: 0; top: 6em;}'+
'.leech-badge {cursor: pointer;}'+
'.leech-badge div.popover {display: none !important;}'+
'#ss_quiz .quiz-progress {margin-bottom: 8px; height: 8px; background-color: gray;}'+
'#ss_quiz .quiz-progress .quiz-progress-bar {height: 8px; background-color: white;}'+
'#ss_quiz .quiz-progress .quiz-progress-bar.pulse { animation: pulse 1.5s ease-in-out infinite alternate; }'+
'@keyframes pulse { 0% { box-shadow: 0px 0px 5px white; } 25% { box-shadow: 0px 0px 20px white; } 75% { box-shadow: 0px 0px 20px white; } 100% { box-shadow: 0px 0px 5px white; } }'+
'#ss_quiz_abort { position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 999; }'+
'';
var quiz_html =
'<div id="ss_quiz" class="kanji reading">'+
' <div class="quiz-progress"><div class="quiz-progress-bar"></div></div>'+
' <div class="qwrap">'+
// ' <div class="prev" title="Previous question (Ctrl-Left)"><i class="icon-chevron-left"></i></div>'+
// ' <div class="next"><i class="icon-chevron-right"></i></div>'+
' <div class="question"></div>'+
' <div class="help"></div>'+
' <div class="summary center">'+
' <h3>Summary - <span class="percent">100%</span> Correct <button class="btn requiz" title="Re-quiz wrong items">Re-quiz</button></h3>'+
' <ul class="errors"></ul>'+
' </div>'+
' <div class="round center"><span class="center">Round 1</span></div>'+
' </div>'+
' <div class="qtype"></div>'+
' <div class="answer"><input type="text" value=""></div>'+
'</div>';
$('head').append('<style type="text/css">'+css+'</style>');
var api_key;
var quiz;
var dialog;
var correct = [];
var incorrect = [];
var wanakana_isbound;
var quizInProgress = false;
function clear() {
$('.leech-badge').remove();
$('<li class="reviews leech-badge"><a><span> </span>Leeches</a></li>').insertAfter('.reviews');
}
function query() {
clear();
if (localStorage.leech_train_cache) {
render(JSON.parse(localStorage.leech_train_cache));
}
get_api_key().then(function() {
ajax_retry('https://wanikanitools-golang.curiousattemptbunny.com/leeches/lesson?api_key='+api_key, {timeout: 0}).then(function(json) {
clear();
render(json);
});
});
}
function render(json) {
localStorage.leech_train_cache = JSON.stringify(json);
$('.leech-badge a span').html(json.leeches_available);
if (quizInProgress) {
return;
}
quiz = json.leech_lesson_items;
$('.leech-badge').click(startQuiz);
}
function startQuiz() {
if (quiz.length === 0) return;
quizInProgress = true;
for(var i=0; i<3; i++) {
shuffle(quiz);
var last = null;
var duplicate = false;
quiz.forEach(function(leech) {
var key = leech.type + "/" + leech.name;
if (key == last) {
duplicate = true;
}
last = key;
});
if (!duplicate) break;
}
correct = [];
incorrect = [];
$('#ss_quiz, #ss_quiz_abort').remove();
$('body').append(quiz_html).append('<div id="ss_quiz_abort"/>');
$('.navbar, #search, .dashboard, footer').css('filter', 'blur(20px)');
wanakana_isbound = false;
dialog = $('#ss_quiz');
$('.quiz-progress-bar').css('width', (correct.length*100.0 / (quiz.length))+'%');
dialog.find('.answer input').on('keypress', onKeyPress);
$('#ss_quiz_abort').click(function() {
$('.navbar, #search, .dashboard, footer').css('filter', 'none');
$('#ss_quiz, #ss_quiz_abort').remove();
quizInProgress = false;
query();
});
show_next();
}
function onKeyPress(e) {
var code = e.originalEvent.code || String.fromCharCode(e.charCode);
if (code === 'Enter') {
var answerGiven = $('#ss_quiz .answer input').val().trim();
if (e.ctrlKey) answerGiven = quiz[0].correct_answers[0];
if (answerGiven.length === 0) return;
if (quiz[0].train_type == 'reading') {
answerGiven = wanakana.toHiragana(answerGiven).trim();
if (answerGiven.indexOf("n") == answerGiven.length-1) {
answerGiven = answerGiven.substring(0,answerGiven.length-1)+"ん";
}
}
$('#ss_quiz .answer input').val(answerGiven);
var correctAnswers = quiz[0].correct_answers;
var tryAgainAnswers = quiz[0].try_again_answers;
var matches = function(answer) {
if (quiz[0].train_type == 'reading') {
return answer == answerGiven;
} else {
return jw_distance(answer.toLowerCase(), answerGiven.toLowerCase()) > 0.9;
}
};
if (correctAnswers.filter(matches).length > 0) {
$('#ss_quiz .answer input').addClass('correct').blur();
correct.push(quiz[0].leech);
quiz = quiz.slice(1);
if (e.ctrlKey) {
show_next();
} else {
setTimeout(show_next, 750);
}
} else if (tryAgainAnswers.filter(matches).length > 0) {
shake($('#ss_quiz .answer input'));
} else {
shake($('#ss_quiz .answer input'), function(e) {if (e) e.focus()});
dialog.find('.help').html('<a href="/'+quiz[0].type+'/'+quiz[0].name+'" target="_blank">'+quiz[0].correct_answers[0]+'</a>').attr('lang','ja').show();
incorrect.push(quiz[0].leech);
}
} else {
dialog.find('.help').hide();
}
$('.quiz-progress-bar').animate({width: (correct.length*100.0 / (correct.length+quiz.length))+'%'}, 250);
}
function shake(elem, callback) {
var dist = '25px';
var speed = 75;
var right = {padding:'0 '+dist+' 0 0'}, left = {padding:'0 0 0 '+dist}, center = {padding:"0 0 0 0"};
if (callback) callback = callback.bind(null, elem);
elem.animate(left,speed/2).animate(right,speed)
.animate(left,speed).animate(right,speed)
.animate(left,speed).animate(center,speed/2, callback);
}
function show_next() {
if (quiz.length === 0) {
$('.quiz-progress-bar').addClass('pulse');
$('#ss_quiz_abort').css('z-index', 1031);
var trainedLeeches = [];
correct.forEach(function(leech) {
if (!trainedLeeches.find(function(l) { return l.key == leech.key; }) && !incorrect.find(function(l) { return l.key == leech.key; })) {
trainedLeeches.push(leech);
}
});
// console.log("CORRECT", correct);
// console.log("WRONG", incorrect);
// console.log("TRAINED", trainedLeeches);
var msg = (trainedLeeches.length === 0 ? "Sorry. No leeches trained." : trainedLeeches.length+" leech"+(trainedLeeches > 1 ? "es" : "")+" trained!");
dialog.find('.help').html(msg).attr('lang','en').show();
var extras = JSON.parse(window.localStorage['leeches-trained'] || '{}');
Object.keys(extras).forEach(function(key) {
if (!trainedLeeches.find(function(l) { return l.key == key; }) && !incorrect.find(function(l) { return l.key == key; })) {
trainedLeeches.push({key: key, worst_incorrect: extras[key]});
}
});
console.log(trainedLeeches);
ajax_retry('https://wanikanitools-golang.curiousattemptbunny.com/leeches/trained?api_key='+api_key, {data: JSON.stringify(trainedLeeches), method: 'POST', timeout: 0}).then(function(json) {
console.log(json);
delete window.localStorage['leeches-trained'];
});
delete localStorage.leech_train_cache;
clear();
return;
}
var item = quiz[0];
var qtype = 'char';
var qlang = 'ja';
var qtext = item.name;
var atype = item.train_type;
var alang = 'ja';
var itype = item.type;
//console.log(item.readings);
dialog.find('.question').attr('data-type', qtype).attr('lang',qlang).html(qtext);
var type_text = itype + ' <strong>'+atype+'</strong>';
dialog.find('.qtype').removeClass('reading meaning').addClass(atype).html(type_text);
dialog.removeClass('kanji vocabulary').addClass(itype);
$('#ss_quiz .answer input').attr('lang',alang).removeClass('correct').val('').focus().select();
if (atype === 'reading') {
if (!wanakana_isbound) {
wanakana.bind($('#ss_quiz .answer input')[0]);
wanakana_isbound = true;
}
} else {
if (wanakana_isbound) {
wanakana.unbind($('#ss_quiz .answer input')[0]);
wanakana_isbound = false;
}
}
}
query();
function shuffle(array) {
var i = array.length, j, temp;
if (i===0) return array;
while (--i) {
j = Math.floor(Math.random()*(i+1));
temp = array[i]; array[i] = array[j]; array[j] = temp;
}
return array;
}
// Jaro-Winkler Distance
function jw_distance(a, c) {
var h, b, d, k, e, g, f, l, n, m, p;
if (a.length > c.length) {
c = [c, a];
a = c[0];
c = c[1];
}
k = ~~Math.max(0, c.length / 2 - 1);
e = [];
g = [];
b = n = 0;
for (p = a.length; n < p; b = ++n) {
for (h = a[b], l = Math.max(0, b - k), f = Math.min(b + k + 1, c.length), d = m = l; l <= f ? m < f : m > f; d = l <= f ? ++m : --m) {
if (g[d] === undefined && h === c[d]) {
e[b] = h;
g[d] = c[d];
break;
}
}
}
e = e.join("");
g = g.join("");
d = e.length;
if (d) {
b = f = k = 0;
for (l = e.length; f < l; b = ++f) {
h = e[b];
if (h !== g[b]) k++;
}
b = g = e = 0;
for (f = a.length; g < f; b = ++g) {
if (h = a[b], h === c[b])
e++;
else
break;
}
a = (d/a.length + d/c.length + (d - ~~(k/2))/d)/3;
a += 0.1 * Math.min(e, 4) * (1 - a);
} else {
a = 0;
}
return a;
}
//-------------------------------------------------------------------
// Fetch a document from the server.
//-------------------------------------------------------------------
function ajax_retry(url, options) {
//console.log(url, retries, timeout);
options = options || {};
var retries = options.retries || 3;
var timeout = options.timeout || 3000;
var headers = options.headers || {};
var method = options.method || 'GET';
var data = options.data || undefined;
var cache = options.cache || false;
function action(resolve, reject) {
$.ajax({
url: url,
method: method,
timeout: timeout,
headers: headers,
data: data,
cache: cache
})
.done(function(data, status){
//console.log(status, data);
if (status === 'success') {
resolve(data);
} else {
//console.log("done (reject)", status, data);
reject();
}
})
.fail(function(xhr, status, error){
//console.log(status, error);
if ((status === 'error' || status === 'timeout') && --retries > 0) {
//console.log("fail", status, error);
action(resolve, reject);
} else {
reject();
}
});
}
return new Promise(action);
}
function get_api_key() {
return new Promise(function(resolve, reject) {
api_key = localStorage.getItem('apiKey_v2');
if (typeof api_key === 'string' && api_key.length == 36) return resolve();
// status_div.html('Fetching API key...');
ajax_retry('/settings/account').then(function(page) {
// --[ SUCCESS ]----------------------
// Make sure what we got is a web page.
if (typeof page !== 'string') {return reject();}
// Extract the user name.
page = $(page);
// Extract the API key.
api_key = page.find('#user_api_key_v2').attr('value');
if (typeof api_key !== 'string' || api_key.length !== 36) {
return reject(new Error('generate_apikey'));
}
localStorage.setItem('apiKey_v2', api_key);
resolve();
},function(result) {
// --[ FAIL ]-------------------------
reject(new Error('Failed to fetch API key!'));
});
});
}
})();