// ==UserScript==
// @name Vocabulary for Wanikani
// @namespace org.dimwits
// @version 1.1.7
// @description Adds vocabulary to the wanikani dashboard
// @include https://www.wanikani.com/dashboard
// @include https://www.wanikani.com
// @author Eekone
// @grant none
// ==/UserScript==
(function() {
const LEVELS_TO_RETRIEVE = 4;
class WordElement {
constructor(word) {
this.word = word;
this.el = document.createElement('a');
this.el.setAttribute('lang', 'ja');
this.el.setAttribute('rel', 'auto-popover');
if (!word.isMarker) {
this.el.setAttribute('href', `/vocabulary/${this.word.character}`);
}
let parent = document.createElement('li');
parent.setAttribute('style', `
background-color: rgba(148, 0, 255, 0.4);
border: ${this.word.highlight}px solid red;
border-radius: 5px;
height: 28px;
z-index: 2;
`);
this.upperBar = document.createElement('div');
this.lowerBar = document.createElement('div');
parent.appendChild(this.el);
parent.appendChild(this.upperBar);
parent.appendChild(this.lowerBar);
this.determineProgressBarLength();
this.setProgress(this.progressBarLength);
let radius = `${(this.progressBarLength.top === 0) ? 5 : 0}px
${(this.progressBarLength.top === 100) ? 0 : 5}px
${(this.progressBarLength.bottom === 100) ? 0 : 5}px
${(this.progressBarLength.bottom > 0) ? 0 : 5}px`;
this.el.setAttribute('style', `
position: relative;
float: left;
margin: 3px;
background-color: ${this.word.color};
border-radius: ${radius};
font-size: 1.2em;
padding: 1px;
z-index: 2;
box-shadow: 0 0 0 0;
-webkit-box-shadow: 0 0 0 0;
flex-grow: 1;
`);
this.el.innerHTML = word.character;
this.wrapper = document.createElement('li');
this.wrapper.setAttribute('style', 'height: auto;');
this.wrapper.appendChild(parent);
if (word.isMarker) return;
this.el.addEventListener('mouseover', this.showPopUp.bind(this));
this.el.addEventListener('mouseleave', this.hidePopUp.bind(this));
}
determineProgressBarLength() {
this.progressBarLength = {top: 50, bottom: 100};
switch (this.word.srsLevel) {
case 4:
this.progressBarLength.bottom = 0;
break;
case 3:
this.progressBarLength.bottom = 50;
break;
case 2: break;
case 1:
this.progressBarLength.top = 100;
break;
default:
this.progressBarLength.top = 100;
}
}
setProgress(barLength = {top: 0, bottom: 0}) {
let upperTopLeftRadius = (barLength.top === 50) ? 0 : 5;
let bottomBottomRightRadius = (barLength.bottom === 50) ? 0 : 5;
this.upperBar.setAttribute('style', `
background-color: ${this.word.color};
border-radius: 5px ${upperTopLeftRadius}px 0px 0px;
top: 0px;
width: ${barLength.top}%;
height: 50%;
z-index: 1;
`);
this.lowerBar.setAttribute('style', `
background-color: ${this.word.color};
border-radius: 0px 0px ${bottomBottomRightRadius}px 5px;
top: 50%;
width: ${barLength.bottom}%;
height: 50%;
z-index: 1;
`);
}
getCoordinates() {
let coords = { left: 0, top: 0 };
coords.left += this.el.offsetLeft;
coords.top += this.el.offsetTop;
return coords;
}
showPopUp() {
const coords = this.getCoordinates();
const bBox = this.el.getBoundingClientRect();
const width = bBox.right - bBox.left;
const height = bBox.bottom - bBox.top;
popOverWindow.setCoordinatesAndShow(coords.left, coords.top - height-5, width);
popOverWindow.setWord(this.word);
}
hidePopUp() {
popOverWindow.hide();
}
attachTo(element) {
element.appendChild(this.wrapper);
}
}
class PopOverWindow {
constructor(container) {
this.el = document.createElement('div');
this.style = '';
this.container = container;
this.buildHTML();
}
buildHTML() {
this.el.setAttribute('class', 'popover lattice right in');
this.popoverInner = document.createElement('div');
this.popoverInner.setAttribute('class', 'popover-inner');
let arrow = document.createElement('div');
arrow.setAttribute('class', 'arrow');
this.popoverInner.appendChild(arrow);
this.popoverTitle = document.createElement('h3');
this.popoverTitle.setAttribute('class', 'popover-title');
this.popoverMeaning = document.createElement('span');
this.popoverTitle.appendChild(this.popoverMeaning);
this.popoverKana = document.createElement('span');
this.popoverKana.setAttribute('lang', 'ja');
this.popoverTitle.appendChild(this.popoverKana);
this.popoverInner.appendChild(this.popoverTitle);
let contentContainer = document.createElement('div');
contentContainer.setAttribute('class', 'popover-content');
this.el.appendChild(this.popoverInner);
this.el.appendChild(contentContainer);
}
show() {
this.container.appendChild(this.el);
}
hide() {
this.container.removeChild(this.el);
}
setCoordinatesAndShow(left, top, elementWidth) {
this.container.appendChild(this.el);
this.width = this.el.getBoundingClientRect().right - this.el.getBoundingClientRect().left;
if (left + this.width > window.innerWidth * 0.95) {
this.left = left - this.width;
this.el.setAttribute('class', 'popover lattice left in');
} else {
this.left = left + elementWidth;
this.el.setAttribute('class', 'popover lattice right in');
}
this.el.setAttribute('style', `
top: ${top}px;
left: ${this.left}px;
display: block;
`);
}
setWord(word) {
this.popoverMeaning.innerHTML = word.meaning + '<br>';
this.popoverKana.innerHTML = word.kana + '<br>';
this.popoverKana.innerHTML += `
<b style="font-size: 0.75em;">
${word.nextReview}
`;
}
}
class Tamperer {
constructor() {
this.baseURL = `https://www.wanikani.com/`;
this.vocabulary = [];
this.getApiKey().then(() => {
this.getLevel().then((level) => {
this.level = level;
let levelString = '';
for(let i = this.level; i >= 0 && i > this.level - LEVELS_TO_RETRIEVE; i--)
levelString += `${i},`;
this.buildVocab(levelString.slice(0, -1)).then(() => {
this.visualize();
});
});
});
}
getApiKey() {
return new Promise((resolve, reject) => {
this.apiKey = localStorage.getItem('apiKey');
if (this.apiKey !== null && this.apiKey.length === 32) {
resolve();
return;
}
this.sendRequest('GET', '/account').then((response) => {
let pattern = new RegExp('<input value="([a-z0-9]{32}).*\n.*/api/user/generate_key');
this.apiKey = pattern.exec(response)[1];
localStorage.setItem('apiKey', this.apiKey);
resolve();
});
});
}
getLevel() {
return new Promise ((resolve, reject) => {
this.sendRequest('GET', `api/user/${this.apiKey}/user-information`).then((userInfo) => {
const info = JSON.parse(userInfo);
resolve(info.user_information.level);
})
.catch(() => alert('Something has gone south when obtaining level'));
});
}
getOuterContainer() {return document.querySelector('.progression');}
buildVocab(level) {
const currentDate = new Date();
return new Promise((resolve, reject) => {
this.sendRequest('GET', `api/user/${this.apiKey}/vocabulary/${level}`).then((list) => {
const vocabList = JSON.parse(list).requested_information;
let previousWord = null;
//Delete all unnecessary elements
for (let i = vocabList.length - 1; i >= 0; i--) {
if (vocabList[i].user_specific === null) {
vocabList.splice(i, 1);
}
}
vocabList.sort((left, right) => {
return (left.level - right.level === 0) ? left.user_specific.available_date - right.user_specific.available_date : right.level - left.level;
});
vocabList.forEach((value) => {
if (value.user_specific !== null &&
value.user_specific.srs_numeric <= 4) {
let word = {};
word.character = value.character;
word.kana = value.kana;
word.meaning = value.meaning.charAt(0).toUpperCase() + value.meaning.split(', ')[0].slice(1);
word.level = value.level;
word.srsLevel = value.user_specific.srs_numeric;
word.color = '#9400ff';
word.highlight = (word.srsLevel > 1) ? 0 : 1;
word.availableDate = value.user_specific.available_date;
word.nextReview =this.formatDate(new Date(value.user_specific.available_date * 1000));
if (previousWord === null || previousWord.level !== word.level) {
let marker = {};
marker.character = word.level;
marker.color = '#434343';
marker.isMarker = true;
this.vocabulary.push(marker);
}
word.isMarker = false;
this.vocabulary.push(word);
previousWord = word;
}
});
resolve();
})
.catch(() => alert('Something has gone south when obtaining vocab list'));
});
}
formatDate(d){
var s = 'Next: ';
var now = new Date();
var YY = d.getFullYear(),
MM = d.getMonth(),
DD = d.getDate(),
hh = d.getHours(),
mm = d.getMinutes(),
one_day = 24*60*60*1000;
if (d < now) return "Available Now";
var same_day = ((YY == now.getFullYear()) && (MM == now.getMonth()) && (DD == now.getDate()) ? 1 : 0);
if (same_day) {
s += 'Today ';
} else {
s += ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()]+', '+
['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][MM]+' '+DD+', ';
}
s += ('0'+hh).slice(-2)+':'+('0'+mm).slice(-2);
if (!same_day) {
var days = (Math.floor((d.getTime()-d.getTimezoneOffset()*60*1000)/one_day)-Math.floor((now.getTime()-d.getTimezoneOffset()*60*1000)/one_day));
if (days) s += ' ('+days+' day'+(days>1?'s':'')+')';
}
return s;
}
sendRequest(method, relativeURL) {
console.log(relativeURL);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
var api_key = localStorage.getItem('apiKey');
xhr.open(method, this.baseURL + relativeURL);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (this.status == 200) {resolve(xhr.responseText);}
else {reject();}
}
};
});
}
visualize() {
const outerContainer = this.getOuterContainer();
const vocabProgress = document.createElement('div');
const title = document.createElement('h3');
title.innerHTML = `Recent Vocabulary Progression`;
vocabProgress.appendChild(title);
vocabProgress.setAttribute('class', 'vocabulary-progress');
let lattice =document.createElement('div');
lattice.setAttribute('class', 'lattice-multi-character');
vocabProgress.appendChild(lattice);
let levelList = document.createElement('ul');
var flag=0;
this.vocabulary.forEach((word) => {
if (word.isMarker && flag<3) {
levelList = document.createElement('ul');
levelList.setAttribute('style', `
display: flex;
flex-flow: row wrap;
justify-content:space-between;
`);
let after = document.createElement('li');
after.setAttribute('style', `
content: "";
flex: auto;
flex-grow: 100;
order: 1;
`);
levelList.appendChild(after);
lattice.appendChild(levelList);
flag=flag+1;
}
let wordElement = new WordElement(word);
wordElement.attachTo(levelList);
});
outerContainer.appendChild(vocabProgress);
}
}
const popOverWindow = new PopOverWindow(document.getElementsByTagName('body')[0]);
const tamperer = new Tamperer();
})();