// ==UserScript==
// @name Shin WaniKani Leech Trainer
// @namespace http://tampermonkey.net/
// @version 3.4.3
// @description Study and quiz yourself on your leeches!
// @author Ross Hendry (rhendry@gmail.com)
// @match https://www.wanikani.com/
// @match https://www.wanikani.com/dashboard
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @source https://github.com/chooban/wk-leeches-go
// @license MIT
// @homepage https://greasyfork.org/en/scripts/372086-shin-wanikani-leech-trainer
// @include *preview.wanikani.com*
// @run-at document-end
// @require https://unpkg.com/wanakana@4.0.2/umd/wanakana.min.js
// ==/UserScript==
// src/appstore.js
function appStore() {
// Hook into App Store
(function appStore() {
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) { }
})();
}
// src/config.js
// It's crap to include test code in production, but it's a workaround
// for now to stop me pushing out stuff pointing to localhost.
const { name } = GM_info.script
console.log('Script name is', name)
const config = {
BASE_URL: name.startsWith('Local')
? 'https://leeches.local/api'
: 'https://leeches.rosshendry.com',
KEY_API_KEY: 'wkApiKeyV2',
KEY_LEECH_CACHE: 'wkLeechCache',
KEY_LEECHES_TRAINED: 'wkLeechesTrained',
}
{
config
}
// src/shake.js
function shake(elem) {
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" };
elem.animate(left, speed / 2).animate(right, speed)
.animate(left, speed).animate(right, speed)
.animate(left, speed).animate(center, speed / 2);
}
{ shake }
// src/util/jarowinkler.js
// 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;
}
{ jw_distance }
// src/lib/quiz/Question.js
const CORRECT = 'correct'
const INCORRECT = 'incorrect'
const TRY_AGAIN = 'try_again'
class Question {
constructor(data) {
this.name = data.name
this.type = data.type
this.trainingType = data.train_type
this.correctAnswers = data.correct_answers
this.tryAgainAnswers = data.try_again_answers
this.key = data.type + "/" + data.name
this.isSimilar = data.is_similar
this.originalLeech = data
}
checkAnswer(answer) {
if (!answer || answer.length === 0) {
return INCORRECT
}
if (this.trainingType === 'reading') {
// Were we given kana?
const isKana = wanakana.isKana(answer)
if (!isKana) {
return TRY_AGAIN
}
// Since we know it's kana, check to see if it's correct
return this.correctAnswers.includes(answer) ? CORRECT : INCORRECT
}
// It's a meaning question
const closeEnoughMatch = function (givenAnswer, answer) {
return jw_distance(answer.toLowerCase(), givenAnswer.toLowerCase()) > 0.9;
}
if (this.correctAnswers.filter(closeEnoughMatch.bind(this, answer)).length > 0) {
return CORRECT
} else if (this.tryAgainAnswers.filter(closeEnoughMatch.bind(this, answer)).length > 0) {
return TRY_AGAIN
} else {
return INCORRECT
}
}
}
{ Question }
// src/util/shuffle.js
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;
}
{
shuffle
}
// src/lib/quiz/Quiz.js
class Quiz {
constructor(leechItems) {
this.lessons = leechItems.filter(l => l.name !== undefined && l.name.length > 0)
this.questions = this.makeQuestions(this.lessons)
this.correctAnswers = []
this.incorrectAnswers = []
this.nthQuestion = 0
}
makeQuestions(lessons) {
if (!lessons || lessons.length == 0) {
throw new Error("Cannot make a quiz with no questions")
}
let questions = lessons.map(function (lesson) {
if (!lesson.is_similar) {
return new Question(lesson)
}
}).filter(Boolean)
let similars = lessons.map(function (lesson) {
if (lesson.is_similar) {
return new Question(lesson)
}
}).filter(Boolean)
questions = [...questions, ...questions, ...questions, ...similars]
for (let i = 0; i < 3; i++) {
shuffle(questions);
let previousKey = null;
let duplicate = false;
questions.forEach(function (leech) {
var key = leech.type + "/" + leech.name;
leech.key = key
if (key === previousKey) {
duplicate = true;
}
previousKey = key;
});
if (!duplicate) break;
}
return questions
}
items() {
return this.lessons.length
}
similars() {
return this.lessons.filter(function(lesson) {
return lesson.is_similar
})
}
currentQuestion() {
return this.questions[this.nthQuestion]
}
lastQuestion() {
return this.questions[this.nthQuestion - 1]
}
advanceQuestion(answerStatus) {
if (answerStatus === CORRECT) {
this.correctAnswers.push(this.questions[this.nthQuestion])
this.nthQuestion += 1
} else if (answerStatus === INCORRECT) {
this.incorrectAnswers.push(this.questions[this.nthQuestion])
}
return answerStatus
}
percentComplete() {
return (this.correctAnswers.length / this.questions.length) * 100
}
length() {
return this.questions.length
}
/**
* Checks the provided answer against the current question.
* There are shades of grey in determining if the answer is correct or not,
* so this isn't a boolean return
*
* @param {string} answer
* @returns {string} One of CORRECT, INCORRECT, or TRY_AGAIN.
*/
submitAnswer(answer) {
const q = this.currentQuestion()
return this.advanceQuestion(q.checkAnswer(answer))
}
/**
* Returns an array of the lessons which have been successfully trained.
*/
trained() {
const trained = []
const self = this
this.correctAnswers.forEach(function (leech) {
if (!trained.find(function (l) { return l.key == leech.key; })
&& !self.incorrectAnswers.find(function (l) { return l.key == leech.key; })
) {
trained.push(leech);
}
});
return trained.map(function (l) {
return l.originalLeech
})
}
}
{ Quiz, CORRECT, INCORRECT, TRY_AGAIN }
// src/index.js
(function() {
'use strict';
const { KEY_API_KEY } = config
const quizHtml = `
<div id="leech_quiz" class="kanji reading">
<div class="quiz-progress"><div class="quiz-progress-bar"></div></div>
<div class="qwrap">
<div class="question"></div>
<div class="help"></div>
</div>
<div class="qtype"></div>
<div class="answer"><input type="text" value=""></div>
</div>`;
var quiz;
var wanakanaIsBound;
var quizInProgress = false;
GM_registerMenuCommand("WaniKani Leech Trainer: Set API key", promptApiKey);
GM_registerMenuCommand("WaniKani Leech Trainer: Set Leech Score", promptLeechScore);
GM_registerMenuCommand("WaniKani Leech Trainer: Set Quiz Size", promptQuizSize);
GM_registerMenuCommand("WaniKani Leech Trainer: Squash My Leeches", squashMyLeeches);
GM_registerMenuCommand("WaniKani Leech Trainer: XXX! DELETE MY STATS", promptDelete);
function squashMyLeeches() {
var apiKey = GM_getValue(KEY_API_KEY) || ''
var confirmation = window.prompt("Do you really want to squash all your leeches? Enter 'yes' to confirm", "no")
if (confirmation === null || confirmation === "no") {
;
} else if (typeof confirmation === 'string' && confirmation === 'yes') {
ajaxRetry(config.BASE_URL + '/leeches/squash', {
headers: {
'Authorization': `Bearer ${apiKey}`
},
method: 'POST'
}).then(refreshLessons)
.catch(function(error) {
console.log(error)
console.log("Failed to squash leeches")
})
}
}
function promptDelete() {
var apiKey = GM_getValue(KEY_API_KEY) || ''
ajaxRetry(config.BASE_URL + '/user', {
headers: {
'Authorization': `Bearer ${apiKey}`
},
}).then(() => {
var confirmation = null
confirmation = window.prompt("Do you really want to reset your stats? Enter 'yes' to confirm", "no")
if (confirmation === null || confirmation === "no") {
;
} else if (typeof confirmation === 'string' && confirmation === 'yes') {
ajaxRetry(config.BASE_URL + '/leeches', {
headers: {
'Authorization': `Bearer ${apiKey}`
},
method: 'DELETE'
}).then(() => {
refreshLessons()
})
}
})
}
function promptApiKey() {
var currentApiKey = GM_getValue(KEY_API_KEY) || ''
var possibleApiKey = null
while (true) {
possibleApiKey = window.prompt("Please enter your API key", currentApiKey)
if (typeof possibleApiKey === 'string' && possibleApiKey.length === 36) {
GM_setValue(KEY_API_KEY, possibleApiKey)
break
} else if (possibleApiKey === null) {
// User clicked cancel
break
} else {
alert("That does not look like a valid key, please try again")
}
}
}
function promptLeechScore() {
var apiKey = GM_getValue(KEY_API_KEY) || ''
ajaxRetry(config.BASE_URL + '/user', {
headers: {
'Authorization': `Bearer ${apiKey}`
},
}).then((profile) => {
let possibleNewScore = null
while (true) {
possibleNewScore = window.prompt("Please set your leech score", profile.leech_score)
if (parseFloat(possibleNewScore) !== NaN && parseFloat(possibleNewScore) >= 1.0) {
setProfileValue('leech_score', parseFloat(possibleNewScore))
break
} else if (possibleNewScore === null) {
break
} else {
alert("Doesn't look right. Please try again")
}
}
})
}
function promptQuizSize() {
var apiKey = GM_getValue(KEY_API_KEY) || ''
ajaxRetry(config.BASE_URL + '/user', {
headers: {
'Authorization': `Bearer ${apiKey}`
},
}).then((profile) => {
let possibleNewQuizSize = null
while (true) {
possibleNewQuizSize = window.prompt("Please set your quiz size", profile.quiz_size)
if (parseInt(possibleNewQuizSize) !== NaN && parseFloat(possibleNewQuizSize) >= 1.0) {
setProfileValue('quiz_size', parseInt(possibleNewQuizSize))
break
} else if (possibleNewQuizSize === null) {
break
} else {
alert("Doesn't look right. Please try again")
}
}
})
}
function setProfileValue(key, score) {
var apiKey = GM_getValue(KEY_API_KEY) || ''
ajaxRetry(config.BASE_URL + '/user', {
headers: {
'Authorization': `Bearer ${apiKey}`
},
method: 'PATCH',
data: JSON.stringify({
[key]: score
})
}).finally(refreshLessons)
.catch(e => {
console.error(e)
})
}
function loading() {
$('.sitemap__section__leeches').remove();
var leechButton = `
<li class="sitemap__section sitemap__section__leeches">
<h2 class="sitemap__section-header sitemap__section-header--leeches" data-navigation-section-toggle="" data-expanded="false" role="button">
<div class="spinner">
<span lang="ja">蛭達</span>
<span lang="en">Leeches</span>
</div>
</h2>
</li>`
var parentElement = $('.navigation .sitemap__section-header--vocabulary').parent()
if (!parentElement.length) {
console.log('Could not find the vocabulary button to attach to')
return
}
var btnElement = $(leechButton)
btnElement.insertAfter(parentElement)
}
function renderError(error) {
$('.sitemap__section__leeches').remove();
var leechButton = `
<li class="sitemap__section sitemap__section__leeches">
<h2 class="sitemap__section-header sitemap__section-header--leeches" data-navigation-section-toggle="" data-expanded="false" role="button">
<span lang="ja">蛭達</span>
<span lang="en">Leeches - Error!</span>
</h2>
</li>`
var parentElement = $('.navigation .sitemap__section-header--vocabulary').parent()
if (!parentElement.length) {
console.log('Could not find the vocabulary button to attach to')
return
}
var btnElement = $(leechButton)
btnElement.click(function(event) {
event.stopImmediatePropagation()
window.alert(error)
})
btnElement.insertAfter(parentElement)
}
function renderButton(json) {
if (quizInProgress) {
return;
}
$('.sitemap__section__leeches').remove();
var leechButton = `
<li class="sitemap__section sitemap__section__leeches">
<h2 class="sitemap__section-header sitemap__section-header--leeches" data-navigation-section-toggle="" data-expanded="false" role="button">
<span lang="ja">蛭達</span>
<span lang="en">Leeches</span>
</h2>
<div class="sitemap__expandable-chunk sitemap__expandable-chunk--leeches" data-navigation-section-content="" data-expanded="false" aria-expanded="false">
<ul class="sitemap__pages sitemap__pages--leeches">
<li class="sitemap__page sitemap__page--leech">
You have <span class="leech-count">${json.stats.leech_count}</span> leeches
</li>
<li class="sitemap__page sitemap__page--leech">
<button style="width: 100%;" class="leeches-start-quiz">Squash some leeches!</button>
</li>
</ul>
</div>
</li>`
var parentElement = $('.navigation .sitemap__section-header--vocabulary').parent()
if (!parentElement.length) {
console.log('Could not find the vocabulary button to attach to')
return
}
var btnElement = $(leechButton)
btnElement.click(function(event) {
event.stopImmediatePropagation()
var header = $(this).find('h2.sitemap__section-header')
var sitemap = $(this).find('div.sitemap__expandable-chunk')
var toggleTo = header.attr('data-expanded') === 'true' ? 'false' : 'true'
header.attr('data-expanded', toggleTo)
sitemap.attr('data-expanded', toggleTo)
})
btnElement.insertAfter(parentElement)
if (json.lessons.length > 0) {
quiz = new Quiz(json.lessons);
$('.navigation .sitemap__section__leeches button').click(startQuiz);
} else {
$('.navigation .sitemap__section__leeches button').remove()
}
}
function refreshLessons() {
loading();
getAPIKey()
.then(function(apiKey) {
ajaxRetry(config.BASE_URL + '/leeches/lesson', {
timeout: 0,
headers: {
'Authorization': `Bearer ${apiKey}`
}
})
.then(renderButton)
.catch(function(error) {
console.log("Failed to retrieve lessons")
console.log(error)
renderError(error)
})
}).catch(function(error) {
console.log("Failed to get API key");
});
}
function startQuiz(e) {
// The default action is to navigate to /, so let's not do that.
e.preventDefault();
if (quiz.length() === 0) return;
quizInProgress = true;
$('#leech_quiz, #leech_quiz_abort').remove();
$('body').append('<div id="leech_quiz_abort"/>').append(quizHtml);
$('.navbar, #search, .dashboard, footer').css('filter', 'blur(20px)');
wanakanaIsBound = false;
$('.quiz-progress-bar').animate({ width: quiz.percentComplete() + '%' }, 250);
$('#leech_quiz').find('.answer input').on('keypress', onAnswerKeyPress);
$('#leech_quiz_abort').click(closeQuiz);
showNextQuestion();
}
function closeQuiz() {
$('.navbar, #search, .dashboard, footer').css('filter', 'none');
$('#leech_quiz, #leech_quiz_abort').remove();
quizInProgress = false;
refreshLessons();
}
function onAnswerKeyPress(e) {
var code = e.originalEvent
? (e.originalEvent.charCode ? e.originalEvent.charCode : e.originalEvent.keyCode ? e.originalEvent.keyCode : 0)
: (e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0);
if ($('#leech_quiz').find('.help').is(':visible')) {
$('#leech_quiz').find('.help').hide();
$('#leech_quiz .answer input').val('').focus().select();
} else if (code === 13) {
var answerGiven = $('#leech_quiz .answer input').val().trim();
// if (e.ctrlKey) answerGiven = quiz[0].correct_answers[0];
if (answerGiven.length === 0) return;
let question = quiz.currentQuestion()
if (question.trainingType === 'reading') {
answerGiven = wanakana.toHiragana(answerGiven).trim();
if (answerGiven.indexOf("n") == answerGiven.length - 1) {
answerGiven = answerGiven.substring(0, answerGiven.length - 1) + "ん";
}
}
$('#leech_quiz .answer input').val(answerGiven);
const result = quiz.submitAnswer(answerGiven);
if (result === TRY_AGAIN) {
shake($('#leech_quiz .answer input'));
} else if (result === INCORRECT) {
shake($('#leech_quiz .answer input'));
$('#leech_quiz .answer input').select();
let infoUrl = '<a href="/' + question.type + '/' + question.name + '" target="_blank">' + question.correctAnswers[0] + '</a>'
$('#leech_quiz')
.find('.help')
.html(infoUrl)
.attr('lang', (question.type === 'reading') ? 'ja' : 'en')
.show();
} else {
$('#leech_quiz .answer input').addClass('correct').blur();
setTimeout(showNextQuestion, 500);
}
} else {
$('#leech_quiz').find('.help').hide();
}
$('.quiz-progress-bar').animate({ width: quiz.percentComplete() + '%' }, 250);
}
function finishQuiz() {
$('.quiz-progress-bar').addClass('pulse');
$('#leech_quiz_abort').css('z-index', 1031);
var trainedLeeches = quiz.trained();
var trained = trainedLeeches.reduce(function(acc, leech) {
if (leech.is_similar) {
acc.similars.push(leech)
} else {
acc.leeches.push(leech)
}
return acc
}, { leeches: [], similars: [] })
var msg = trained.leeches.length === 0
? "Sorry. No leeches trained."
: trained.leeches.length + " leech" + (trained.leeches.length > 1 ? "es" : "") + " trained!";
if (quiz.similars().length > 0 && trained.leeches.length > 0) {
msg = msg.slice(0, msg.length - 1)
msg += ", and spotted " + trained.similars.length + " similar item" + ((trained.similars.length > 1 || trained.similars.length == 0) ? "s" : "") + "!"
}
$('#leech_quiz').find('.help').html(msg).attr('lang', 'en').show();
getAPIKey().then(function(apiKey) {
ajaxRetry(config.BASE_URL + '/leeches/trained', {
data: JSON.stringify({ trained: trained.leeches }),
method: 'POST',
timeout: 0,
headers: {
'Authorization': `Bearer ${apiKey}`
}
}).catch(function(e) {
console.error("Failed to submit trained leeches")
console.error(e)
}).finally(function() {
setTimeout(function() {
closeQuiz();
}, 2000)
});
})
}
function showNextQuestion() {
if (quiz.percentComplete() === 100) {
finishQuiz()
return;
}
var item = quiz.currentQuestion();
if (item.isSimilar) {
console.log("This is a red herring similar one")
}
var questionType = 'char';
var questionLanguage = 'ja';
var questionText = item.name;
var answerType = item.trainingType;
var answerLanguage = 'ja';
var itemType = item.type;
var dialog = $('#leech_quiz')
dialog.find('.question').attr('data-type', questionType).attr('lang', questionLanguage).html(questionText);
var type_text = itemType + ' <strong>' + answerType + '</strong>';
dialog.find('.qtype').removeClass('reading meaning').addClass(answerType).html(type_text);
dialog.removeClass('kanji vocabulary').addClass(itemType);
$('#leech_quiz .answer input').attr('lang', answerLanguage).removeClass('correct').val('').focus().select();
if (answerType === 'reading') {
if (!wanakanaIsBound) {
wanakana.bind($('#leech_quiz .answer input')[0]);
wanakanaIsBound = true;
}
} else {
if (wanakanaIsBound) {
wanakana.unbind($('#leech_quiz .answer input')[0]);
wanakanaIsBound = false;
}
}
}
refreshLessons();
//-------------------------------------------------------------------
// Fetch a document from the server.
//-------------------------------------------------------------------
function ajaxRetry(url, options) {
//console.log(url, retries, timeout);
options = options || {};
var retries = options.retries || 3;
var timeout = options.timeout || 6000;
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) {
if (status === 'success' || status === 'nocontent') {
resolve(data);
} else {
debugger
reject(data);
}
})
.fail(function(xhr, status, error) {
if ((status === 'error' || status === 'timeout') && --retries > 0) {
action(resolve, reject);
return
}
debugger
reject(xhr.responseText);
});
}
return new Promise(action);
}
function getAPIKey() {
return new Promise(function(resolve, reject) {
var apiKey = GM_getValue(KEY_API_KEY);
if (typeof apiKey === 'string' && apiKey.length == 36) return resolve(apiKey);
// status_div.html('Fetching API key...');
ajaxRetry('/settings/personal_access_tokens').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.
var possibleApiKey = page.find('table#personal-access-tokens-list tbody tr:last-of-type code')[0].innerText;
if (typeof possibleApiKey !== 'string' || possibleApiKey.length !== 36) {
return reject(new Error('generate_apikey'));
}
GM_setValue(KEY_API_KEY, possibleApiKey);
resolve(possibleApiKey);
}, function(result) {
// --[ FAIL ]-------------------------
reject(new Error('Failed to fetch API key!'));
});
});
}
})();
// CSS injection
(function(){
const $style = document.createElement('style');
$style.innerHTML = `/* src/styles.css */
#leech_quiz [lang="ja"] {
font-family: "Meiryo", "Yu Gothic", "Hiragino Kaku Gothic Pro", "TakaoPGothic",
"Yu Gothic", "ヒラギノ角ゴ Pro W3", "メイリオ", "Osaka", "MS PGothic",
"MS Pゴシック", sans-serif;
}
#leech_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;
}
#leech_quiz * {
text-align: center;
}
#leech_quiz .qwrap {
height: 8em;
position: relative;
clear: both;
}
#leech_quiz.radicals .qwrap,
#leech_quiz.radicals .summary .que {
background-color: #0af;
}
#leech_quiz.kanji .qwrap,
#leech_quiz.kanji .summary .que {
background-color: #f0a;
}
#leech_quiz.vocabulary .qwrap,
#leech_quiz.vocabulary .summary .que {
background-color: #a0f;
}
#leech_quiz .prev,
#leech_quiz .next {
display: inline-block;
width: 80px;
color: #fff;
line-height: 8em;
cursor: pointer;
}
#leech_quiz .prev:hover {
background-image: linear-gradient(
to left,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.2)
);
}
#leech_quiz .next:hover {
background-image: linear-gradient(
to right,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.2)
);
}
#leech_quiz .prev {
float: left;
}
#leech_quiz .next {
float: right;
}
#leech_quiz .topbar {
font-size: 0.5em;
line-height: 1em;
color: rgba(255, 255, 255, 0.5);
}
#leech_quiz .settings {
float: left;
padding: 6px 8px;
text-align: left;
line-height: 1.5em;
}
#leech_quiz .settings span[class*="icon-"] {
font-size: 1.3em;
padding: 0 2px;
}
#leech_quiz .settings .ss_audio {
padding-left: 0;
padding-right: 4px;
}
#leech_quiz .settings .ss_typo {
padding-left: 0px;
}
#leech_quiz .settings .ss_done {
font-size: 1.25em;
}
#leech_quiz .settings .ss_pair {
font-weight: bold;
}
#leech_quiz .settings span {
cursor: pointer;
}
#leech_quiz .settings span:hover {
color: rgba(255, 255, 204, 0.8);
}
#leech_quiz .settings span.active {
color: #ffc;
}
#leech_quiz.help .settings .ss_help {
color: #ffc;
}
#leech_quiz .stats_labels {
text-align: right;
font-family: monospace;
}
#leech_quiz .stats {
float: right;
text-align: right;
color: rgba(255, 255, 255, 0.8);
font-family: monospace;
padding: 0 5px;
}
#leech_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;
}
#leech_quiz.round .round {
display: block;
}
#leech_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;
}
#leech_quiz .question[data-type="char"] {
font-size: 2em;
}
#leech_quiz .icon-audio:before {
content: "\\f028";
}
#leech_quiz .question .icon-audio {
font-size: 2.5em;
cursor: pointer;
}
#leech_quiz.summary .question {
display: none;
}
#leech_quiz .qtype {
line-height: 2em;
cursor: default;
text-transform: capitalize;
}
#leech_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;
}
#leech_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;
}
#leech_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;
}
#leech_quiz.help .help {
display: inherit;
}
#leech_quiz .answer {
background-color: #ddd;
padding: 8px;
}
#leech_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;
}
#leech_quiz .answer input.correct {
color: #fff;
background-color: #8c8;
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2);
}
#leech_quiz .answer input.incorrect {
color: #fff;
background-color: #f03;
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2);
}
#leech_quiz.loading .qwrap,
#leech_quiz.loading .answer {
display: none;
}
#leech_quiz .summary {
display: none;
position: absolute;
width: 74%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
font-weight: bold;
}
#leech_quiz.summary .summary {
display: block;
}
#leech_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;
}
#leech_quiz .summary .errors {
position: absolute;
top: 40px;
bottom: 0px;
width: 100%;
margin: 0;
overflow-y: auto;
list-style-type: none;
}
#leech_quiz .summary li {
margin: 4px 0 0 0;
font-size: 0.6em;
font-weight: bold;
line-height: 1.4em;
}
#leech_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;
}
#leech_quiz .summary .ans {
background-color: #fff;
color: #000;
}
#leech_quiz .summary .wrong {
color: #f22;
}
#leech_quiz .btn.requiz {
position: absolute;
top: 6px;
right: 6px;
padding-left: 6px;
padding-right: 6px;
}
#leech_quiz_container {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
#leech_quiz {
position: fixed;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 6em;
}
#leech_quiz .quiz-progress {
margin-bottom: 8px;
height: 8px;
background-color: gray;
}
#leech_quiz .quiz-progress .quiz-progress-bar {
height: 8px;
background-color: white;
}
#leech_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;
}
}
#leech_quiz_abort {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 999;
}
@keyframes spinner {
0% {
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
.spinner {
height: 100vh;
opacity: 1;
position: relative;
transition: opacity linear 0.1s;
}
.spinner::before {
animation: 2s linear infinite spinner;
border: solid 3px #eee;
border-bottom-color: #ef6565;
border-radius: 50%;
content: "";
height: 20px;
left: 50%;
opacity: inherit;
position: absolute;
top: 47.5px;
transform: translate3d(-50%, -50%, 0);
transform-origin: center;
width: 20px;
will-change: transform;
}
`;
document.body.appendChild($style);
})();