WaniKani Upcoming Lessons

Shows section of upcoming radicals, kanji, and vocabulary on lessons page

// ==UserScript==
// @name        WaniKani Upcoming Lessons
// @namespace   goldenchrysus.wanikani.upcominglessons
// @description Shows section of upcoming radicals, kanji, and vocabulary on lessons page
// @author      GoldenChrysus
// @website     https://github.com/GoldenChrysus
// @version     1.1.3
// @include     https://www.wanikani.com/lesson
// @copyright   2018+, Patrick Golden
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

(function() {
	"use strict";

	// Establish important variables
	let wkof       = window.wkof;
	let modules    = "ItemData";
	let queue      = {
		radical    : [],
		kanji      : [],
		vocabulary : []
	};
	let $container = $(`<div id="upcoming-lessons"></div>`);

	// Check for WaniKani Open Framework
	if (!window.wkof) {
		if (confirm("Upcoming Lessons requires WaniKani Open Framework.\nDo you want to be forwarded to the installation instructions?")) {
			window.location.href = "https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549";
		}

		return;
	}

	// Start WaniKani Open Framework
	wkof.include(modules);
	wkof
		.ready(modules)
		.then(initialize);

	/**
	 * Insert the "Upcoming Lessons" section into the DOM
	 */
	function insertContainer() {
		$("#lessons-summary")
			.find("div.pure-g-r")
			.last()
			.before($container);
	}

	/**
	 * Create radicals section and add to container
	 */
	function createRadicals() {
		let radicals           = queue.radical;
		let count              = radicals.length;
		let $radical_container = $(
			`<div class="pure-g-r">
				<div id="radicals" class="pure-u-1">
					<h2><b>${count} <i class="icon-time" style="margin-left:0.25em; margin-right:0.2em;"></i></b> Radicals</h2>
				</div>
			</div>`
		);

		if (count) {
			$radical_container
				.find("#radicals")
				.append(
					`<div>
						<ul id="lesson-radicals">
						</ul>
					</div>`
				);
		}

		let $radical_list = $radical_container.find("#lesson-radicals");

		for (let item of radicals) {
			let character = item.characters;
			let english   = item.meanings[0].meaning;

			// Some radicals are custom-made by WaniKani or have no available text character, so they are rendered as images.
			if (!character && item.character_images) {
				let image = item.character_images[0].url;

				for (const tmp_image of item.character_images) {
					if (tmp_image.metadata.style_name === "32px") {
						image = tmp_image.url;

						break;
					}
				}

				character = `<img src="${image}" alt="${english}" style="width: 0.8em; filter: invert(100%);"/>`;
			}

			let $element = $(
				`<li class="radical" data-en="${english}">
					<a lang="ja" href="/radicals/${english}">${character}</a>
				</li>`
			);
			//0.3em 0.4em 0.212em 0.4em

			$radical_list.append($element);
		}

		$container.append($radical_container);
	}

	/**
	 * Create kanji section and add to container
	 */
	function createKanji() {
		let kanji            = queue.kanji;
		let count            = kanji.length;
		let $kanji_container = $(
			`<div class="pure-g-r">
				<div id="kanji" class="pure-u-1">
					<h2><b>${count} <i class="icon-time" style="margin-left:0.25em; margin-right:0.2em;"></i></b> Kanji</h2>
				</div>
			</div>`
		);

		if (count) {
			$kanji_container
				.find("#kanji")
				.append(
					`<div>
						<ul id="lesson-kanji">
						</ul>
					</div>`
				);
		}

		let $kanji_list = $kanji_container.find("#lesson-kanji");

		for (let item of kanji) {
			let character = item.characters;
			let english   = item.meanings[0].meaning;
			let readings  = [];

			for (let reading of item.readings) {
				if (reading.primary) {
					readings.push(reading.reading);
				}
			}

			readings = readings.join(", ");

			let $element = $(
				`<li class="kanji" data-en="${english}" data-ja="${readings}">
					<a lang="ja" href="/kanji/${character}">${character}</a>
				</li>`
			);

			$kanji_list.append($element);
		}

		$container.append($kanji_container);
	}

	/**
	 * Create vocabulary section and add to container
	 */
	function createVocabulary() {
		let vocabulary            = queue.vocabulary;
		let count                 = vocabulary.length;
		let $vocabulary_container = $(
			`<div class="pure-g-r">
				<div id="vocabulary" class="pure-u-1">
					<h2><b>${count} <i class="icon-time" style="margin-left:0.25em; margin-right:0.2em;"></i></b> Vocabulary</h2>
				</div>
			</div>`
		);

		if (count) {
			$vocabulary_container
				.find("#vocabulary")
				.append(
					`<div>
						<ul id="lesson-vocabulary">
						</ul>
					</div>`
				);
		}

		let $vocabulary_list = $vocabulary_container.find("#lesson-vocabulary");

		for (let item of vocabulary) {
			let character = item.characters;
			let english   = item.meanings[0].meaning;
			let readings  = [];

			for (let reading of item.readings) {
				if (reading.primary) {
					readings.push(reading.reading);
				}
			}

			readings = readings.join(", ");

			let $element = $(
				`<li class="vocabulary" data-en="${english}" data-ja="${readings}">
					<a lang="ja" href="/vocabulary/${character}">${character}</a>
				</li>`
			);

			$vocabulary_list.append($element);
		}

		$container.append($vocabulary_container);
	}

	/**
	 * Create header section and add to container
	 */
	function createHeader() {
		let $header = $(
			`<div class="pure-g-r">
				<header class="pure-u-1">
					<h1><i class="icon-time"></i> Upcoming Lessons</h1>
				</header>
			</div>`
		);

		$container.append($header);
	}

	/**
	 * Handle hovering and clicking on the lesson items.
	 * Copied from WaniKani source.
	 */
	function initializeEventHandlers() {
		$(document).on("click", "#upcoming-lessons a", function(t) {
			let agent = /iPhone|iPod|iPad|Android|BlackBerry/.test(navigator.userAgent);

			if (agent) {
				return t.preventDefault();
			}
		});

		$(document).on("mouseenter", "#upcoming-lessons ul li", function() {
			var e, t, n, i, r, a, o, s;
			
			return r = $(this).height() + 4, a = $(window).width() - $(this).offset().left, i = $(window).height() - $(this).offset().top, n = $("<div></div>", {
				"class": "hover"
			}).appendTo(this), $("<ul></ul>").appendTo($(this).children("div")), $("<li></li>", {
				text: $(this).data("en")
			}).appendTo($(this).find("ul")), $("<li></li>", {
				text: $(this).data("ja")
			}).appendTo($(this).find("ul")), $("<li></li>", {
				text: $(this).data("mc")
			}).appendTo($(this).find("ul")), $("<li></li>", {
				text: $(this).data("rc")
			}).appendTo($(this).find("ul")), o = a > 200 ? (e = "left-side", "auto") : (e = "right-side", "0"), s = i < 100 ? (t = "down-arrow", -1 * (n.height() + r / 2)) : (t = "up-arrow", r), n.css({
				top: s,
				right: o
			}).addClass(t + " " + e);
		});

		$(document).on("mouseleave", "#upcoming-lessons ul li", function() {
			$(this)
				.children("div")
				.remove();
		});
	}

	/**
	 * Load assignment and item data, then process it.
	 */
	function initialize() {
		wkof.ItemData.get_items("assignments").then(processData);
	}

	/**
	 * Filter item data for assigned lessons, then create DOM elements.
	 */
	function processData(data) {
		for (var item of data) {
			// If the item has no assignments, then it isn't assigned.
			if (!item.assignments) {
				continue;
			}

			// If an assignment isn't started or isn't unlocked, it's not an assigned lesson.
			if (item.assignments.started_at || !item.assignments.unlocked_at) {
				continue;
			}

			item.data.sort_date = item.assignments.unlocked_at || item.assignments.started_at;

			queue[item.object].push(item.data);
		}

		for (let type in queue) {
			queue[type].sort((a, b) => {
				if (a.level !== b.level) {
					return (a.level < b.level) ? -1 : 1;
				}

				return (a.lesson_position < b.lesson_position) ? -1 : 1;
			})
		}

		createHeader();
		createRadicals();
		createKanji();
		createVocabulary();
		insertContainer();
		initializeEventHandlers();
	}
}());