-
-
Save chooban/fc8535e3b5ef6cc3770daeb8c5263738 to your computer and use it in GitHub Desktop.
Leech trainer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Neo Shin WaniKani Leech Trainer | |
// @namespace http://tampermonkey.net/ | |
// @version 3.4.0 | |
// @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://wk-leeches-dev.pun7u6s23ol8m.eu-west-2.cs.amazonlightsail.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.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 | |
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) { | |
return new Question(lesson) | |
}) | |
questions = [...questions, ...questions, ...questions] | |
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 | |
} | |
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, KEY_LEECH_CACHE } = 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 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>`; | |
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); | |
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) || '' | |
ajax_retry(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) { | |
setLeechScore(parseFloat(possibleNewScore)) | |
break | |
} else if (possibleNewScore === null) { | |
break | |
} else { | |
alert("Doesn't look right. Please try again") | |
} | |
} | |
}) | |
} | |
function setLeechScore(score) { | |
var apiKey = GM_getValue(KEY_API_KEY) || '' | |
ajax_retry(config.BASE_URL + '/user', { | |
headers: { | |
'Authorization': `Bearer ${apiKey}` | |
}, | |
method: 'PATCH', | |
data: JSON.stringify({ | |
leech_score: score | |
}) | |
}).finally(query) | |
.catch(e => { | |
console.error(e) | |
}) | |
} | |
function clear() { | |
$('.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">X</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) | |
} | |
function query() { | |
clear(); | |
// var leechCache = GM_getValue(KEY_LEECH_CACHE) || '' | |
// if (leechCache) { | |
// render(JSON.parse(leechCache)); | |
// } | |
getAPIKey().then(function (apiKey) { | |
ajax_retry(config.BASE_URL + '/leeches/lesson', { | |
timeout: 0, | |
headers: { | |
'Authorization': `Bearer ${apiKey}` | |
} | |
}) | |
.then(function (json) { | |
clear(); | |
render(json); | |
}); | |
}); | |
} | |
function render(json) { | |
// GM_setValue(KEY_LEECH_CACHE, JSON.stringify(json)); | |
if (quizInProgress) { | |
return; | |
} | |
console.log(json) | |
console.log({ lessons: json.lessons }) | |
quiz = new Quiz(json.lessons); | |
$('.navigation span.leech-count').html(json.stats.leech_count) | |
$('.navigation .sitemap__section__leeches button').click(startQuiz); | |
} | |
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; | |
query(); | |
} | |
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 msg = trainedLeeches.length === 0 | |
? "Sorry. No leeches trained." | |
: trainedLeeches.length + " leech" + (trainedLeeches > 1 ? "es" : "") + " trained!"; | |
$('#leech_quiz').find('.help').html(msg).attr('lang', 'en').show(); | |
getAPIKey().then(function (apiKey) { | |
ajax_retry(config.BASE_URL + '/leeches/trained', { | |
data: JSON.stringify({ trained: trainedLeeches }), | |
method: 'POST', | |
timeout: 0, | |
headers: { | |
'Authorization': `Bearer ${apiKey}` | |
} | |
}).catch(function (e) { | |
console.error(e) | |
}).finally(function () { | |
setTimeout(function () { | |
closeQuiz(); | |
}, 2500) | |
}); | |
}) | |
} | |
function showNextQuestion() { | |
if (quiz.percentComplete() === 100) { | |
finishQuiz() | |
return; | |
} | |
var item = quiz.currentQuestion(); | |
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; | |
} | |
} | |
} | |
query(); | |
//------------------------------------------------------------------- | |
// 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) { | |
if (status === 'success') { | |
resolve(data); | |
} else { | |
reject(); | |
} | |
}) | |
.fail(function (xhr, status, error) { | |
if ((status === 'error' || status === 'timeout') && --retries > 0) { | |
action(resolve, reject); | |
} else { | |
reject(); | |
} | |
}); | |
} | |
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...'); | |
ajax_retry('/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; | |
} | |
`; | |
document.body.appendChild($style); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment