// ==UserScript==
// @name Wanikani: Dashboard Apprentice
// @namespace http://tampermonkey.net/
// @version 1.2.4
// @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 (wkof, $) {
// 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', kana_voc: 'Kana 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, kana_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 = $('<section id="wkda_items"></section>')[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').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;
--color-text: ${['rgb(240, 240, 240)', 'black'][theme]} !important;
}
#wkda_items.dark {
background-color: #232629;
}
#wkda_items > div {
margin-bottom: 10px;
}
#wkda_items {
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: 3;
}
#wkda_items .items .kana_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
)
}
})(window.wkof, window.$)