// ==UserScript==
// @name Wanikani: Dashboard Apprentice
// @namespace http://tampermonkey.net/
// @version 1.2.1
// @description Displays all your apprentice items on the dashboard
// @author Kumirei
// @match https://www.wanikani.com/
// @match https://www.wanikani.com/dashboard
// @match https://preview.wanikani.com/
// @match https://preview.wanikani.com/dashboard
// @grant none
// ==/UserScript==
/*jshint esversion: 8 */
(function() {
// Make sure WKOF is installed
let script_id = "dashboard_apprentice";
if (!wkof) {
var script_name = 'Wanikani: Dashboard Apprentice';
var response = confirm(script_name + ' requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');
if (response) window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
return;
}
// Ready to go
else {
wkof.include('Menu,Settings,ItemData');
wkof.ready('Menu,Settings,ItemData')
.then(load_settings)
.then(install_menu)
.then(add_css)
.then(fetch_items)
.then(display);
}
function install_menu() {
let config = {
name: script_id,
submenu: 'Settings',
title: 'Dashboard Apprentice',
on_click: open_settings
};
wkof.Menu.insert_script_link(config);
}
function open_settings() {
var config = {
script_id: script_id,
title: 'Dashboard Apprentice',
content: {
theme: {type: 'dropdown', label: 'Theme', default: 0, hover_tip: 'Changes the colors of the items', content: {0: "Default", 1: "Breeze Dark",},},
srs_start: {type: 'number', label: 'First SRS stage', default: 1, hover_tip: 'First SRS stage to display.\n-1: Locked items\n0: Items in your lessons\n1-4: Apprentice\n5-6: Guru\n7: Master\n8: Enlightened\n9: Burned',},
srs_end: {type: 'number', label: 'Last SRS stage', default: 4, hover_tip: 'Last SRS stage to display.\n-1: Locked items\n0: Items in your lessons\n1-4: Apprentice\n5-6: Guru\n7: Master\n8: Enlightened\n9: Burned',},
types: {type: 'list', label: 'Item types', multi: true, hover_tip: 'Which items you want to display', default: {rad: true, kan: true, voc: true}, content: {rad: 'Radicals', kan: 'Kanji', voc: 'Vocabulary'},},
},
};
let dialog = new wkof.Settings(config);
dialog.open();
}
function load_settings() {
let defaults = {theme: 0, srs_start: 1, srs_end: 4, types: {rad: true, kan: true, voc: true}};
return wkof.Settings.load(script_id, defaults);
}
// Fetches the items
async function fetch_items() {
let types = Object.entries(wkof.settings[script_id].types).filter(a=>a[1]).map(a=>a[0]);
return wkof.ItemData.get_index(await wkof.ItemData.get_items({wk_items: {options: {assignments: true},filters: {item_type: types}}}), 'srs_stage');
}
// Puts the information on the dashboard
async function display(data) {
let names = {"-1": 'Locked', 0: 'Lessons', 1: 'Apprentice 1', 2: 'Apprentice 2', 3: 'Apprentice 3', 4: 'Apprentice 4', 5: 'Guru 1', 6: 'Guru 2', 7: 'Master', 8: 'Enlightened', 9: 'Burned'};
var elem = $('<div id="wkda_items"></div>')[0];
if (is_dark_theme()) elem.className = 'dark';
let settings = wkof.settings[script_id];
for (var i=settings.srs_start; i<=settings.srs_end; i++) {
if (!data[i]) continue;
var srs_elem = $('<div class="apprentice_'+i+'"></div>')[0];
var title = $('<span>'+names[i]+' </span>')[0];
var items = $('<div class="items"></div>')[0];
srs_elem.appendChild(title);
srs_elem.appendChild(items);
for (var j=0; j<data[i].length; j++) {
var item = data[i][j];
var info = {
type: item.object,
characters: item.data.characters !== null ? item.data.characters : await wkof.load_file(item.data.character_images.find(c => c.content_type === "image/svg+xml" && !c.metadata.inline_styles).url, true),
meanings: [],
readings: [],
level: item.data.level,
url: item.data.document_url,
available: (i==-1?'Locked':i==0?'In lesson queue':item.assignments.srs_stage==9?'Burned':(Date.parse(item.assignments.available_at) < Date.now() ? 'Now' : s_to_dhm((Date.parse(item.assignments.available_at)-Date.now())/1000)))
};
for (let k=0; k<item.data.meanings.length; k++) {
info.meanings.push(item.data.meanings[k].meaning);
}
if (item.data.readings) {
for (let k=0; k<item.data.readings.length; k++) {
info.readings.push(item.data.readings[k].reading);
}
}
var item_elem = $('<div class="item '+info.type+'"'+'>'+
'<div class="hover_elem">'+
'<div class="left">'+
'<a class="'+info.type+'" href="'+info.url+'">'+info.characters+'</a>'+
'</div>'+
'<div class="right">'+
'<table>'+
'<tr><td>Meanings</td><td>'+info.meanings.join(', ')+'</td></tr>'+
'<tr><td>Readings</td><td>'+info.readings.join('、')+'</td></tr>'+
'<tr><td>Level</td><td>'+info.level+'</td></tr>'+
'<tr><td>Available</td><td>'+info.available+'</td></tr>'+
'</table>'+
'</div>'+
'</div>'+
'<a class="'+info.type+'" href="'+info.url+'">'+info.characters+'</a>'+
'</div>')[0];
items.appendChild(item_elem);
}
elem.appendChild(srs_elem);
}
let target = document.querySelector('.span12 > .row');
target.parentElement.insertBefore(elem, target);
}
// Adds the CSS to the page
function add_css() {
let theme = wkof.settings[script_id].theme;
$('head').append('<style id="wkda_css">'+
'#wkda_items {'+
' background-color: #f4f4f4;'+
' border-radius: 5px;'+
' padding: 16px 24px 12px;'+
'}'+
'#wkda_items.dark {'+
' background-color: #232629;'+
'}'+
'#wkda_items > div {'+
' margin-bottom: 10px;'+
'}'+
'#wkda_items > div > span {'+
' font-size: 16px;'+
'}'+
'#wkda_items .items {'+
' position: relative;'+
' display: flex;'+
' flex-direction: row;'+
' flex-wrap: wrap;'+
' justify-content: flex-start;'+
' margin-left: -2px;'+
'}'+
'#wkda_items .items .item {'+
' display: inline-block;'+
' padding: 0 3px;'+
' margin: 1.5px;'+
' border-radius: 3px;'+
' position: relative;'+
'}'+
'#wkda_items .items .radical {'+
' background: '+['#0096e7', '#3daee9'][theme]+';'+
' order: 0;'+
' width: 14px;'+
'}'+
'#wkda_items .items .kanji {'+
' background: '+['#ff00aa', '#fdbc4b'][theme]+';'+
' order: 1;'+
'}'+
'#wkda_items .items .vocabulary {'+
' background: '+['#9800e8', '#2ecc71'][theme]+';'+
' order: 2;'+
'}'+
'#wkda_items .hover_elem {'+
' visibility: hidden;'+
' position: absolute;'+
' background-color: rgba(0, 0, 0, 0.9);'+
' z-index: 2;'+
' padding: 5px;'+
' border-radius: 3px;'+
' width: max-content;'+
' transform: translate(-50%, calc(0px - 100% - 5px));'+
' left: 50%;'+
'}'+
'#wkda_items .item:hover .hover_elem {'+
' visibility: visible; '+
'}'+
'#wkda_items .hover_elem::after {'+
' visibility: hidden;'+
' position: absolute;'+
' width: 0;'+
' border-top: 5px solid rgba(0, 0, 0, 0.9);'+
' border-right: 5px solid transparent;'+
' border-left: 5px solid transparent;'+
' content: " ";'+
' font-size: 0;'+
' line-height: 0;'+
' left: 50%;'+
' bottom: -5px;'+
' transform: translateX(-50%);'+
'}'+
'#wkda_items .item:hover .hover_elem::after {'+
' visibility: visible;'+
'}'+
'#wkda_items .hover_elem > div {'+
' display: inline-block;'+
'}'+
'#wkda_items .item.vocabulary .hover_elem > div {'+
' display: block;'+
'}'+
'#wkda_items .left {'+
' vertical-align: top;'+
'}'+
'#wkda_items .item.vocabulary .hover_elem .left {'+
' margin-bottom: 5px;'+
'}'+
'#wkda_items .left a {'+
' font-size: 74px;'+
' line-height: 73px;'+
' min-width: 73px;'+
' display: block;'+
' padding: 5px;'+
' border-radius: 3px;'+
' margin: 3px 10px 0 3px;'+
'}'+
'#wkda_items .item.vocabulary .left a {'+
' margin-right: 3px;'+
' text-align: center;'+
'}'+
'#wkda_items .items .radical svg {'+
' height: 14px;'+
' stroke: currentColor;'+
' fill: none;'+
' stroke-linecap: square;'+
' stroke-width: 68;'+
'}'+
'#wkda_items .items .radical svg g {'+
' clip-path: none;'+
'}'+
'#wkda_items .items .radical .hover_elem svg {'+
' height: 74px;'+
' width: 1em;'+
'}'+
'#wkda_items .right table td:first-child {'+
' padding-right: 10px;'+
' font-weight: bold;'+
'}'+
'#wkda_items .items table td {'+
' color: rgb(240, 240, 240);'+
'}'+
'#wkda_items .items > div a {'+
' color: '+['rgb(240, 240, 240)', 'black'][theme]+' !important;'+
'}'+
'#wkda_items .item.vocabulary .hover_elem {'+
' max-width: 320px;'+
'}'+
'</style>');
}
// Converts seconds to days, hours, and minutes
function s_to_dhm(s) {
var d = Math.floor(s/60/60/24);
var h = Math.floor(s%(60*60*24)/60/60);
var m = Math.ceil(s%(60*60*24)%(60*60)/60);
return (d>0?d+'d ':'')+(h>0?h+'h ':'')+(m>0?m+'m':'1m');
}
// Returns a promise and a resolve function
function new_promise() {
var resolve, promise = new Promise((res, rej)=>{resolve = res;});
return [promise, resolve];
}
// Handy little function that rfindley wrote. Checks whether the theme is dark.
function is_dark_theme() {
// Grab the <html> background color, average the RGB. If less than 50% bright, it's dark theme.
return $('body').css('background-color').match(/\((.*)\)/)[1].split(',').slice(0,3).map(str => Number(str)).reduce((a, i) => a+i)/(255*3) < 0.5;
}
})();