// ==UserScript==
// @name Wanikani hanzi-writer addition
// @namespace https://declanfodor.com
// @description Replaces kanji in wanikani with hanzi writer. Licenses for kanji data are found at https://github.com/chanind/hanzi-writer-data-jp/
// @match https://www.wanikani.com/*
// @version 0.0.4
// @author Declan Fodor
// @resource kanjiJSON https://raw.githubusercontent.com/chanind/hanzi-writer-data-jp/master/data/all.json
// @require https://cdn.jsdelivr.net/npm/hanzi-writer@3.5/dist/hanzi-writer.js
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2
// @license MIT
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_setvalue
// ==/UserScript==
(function () {
'use strict';
const WK_PAGE = Object.freeze({
REVIEW: Symbol("review_page"),
LESSON: Symbol("lesson_page"),
DASHBOARD: Symbol("dashboard_page"),
LOADING: Symbol("other_page") // Or a page we haven't implemented behavior for yet
});
class PageStatus {
constructor(previousStatus) {
this.page = this.whichPage(unsafeWindow.location.href);
this.switched = previousStatus ? previousStatus.page !== this.page : true;
}
whichPage(url) {
switch (url) {
case "https://www.wanikani.com/":
return WK_PAGE.DASHBOARD;
case "https://www.wanikani.com/subjects/review":
return WK_PAGE.REVIEW;
default:
return WK_PAGE.LOADING;
}
}
}
class PageObserver {
constructor(wk_page, onPage, offPage) {
this.status = new PageStatus(null);
this.observer = new MutationObserver(() => {
if (this.status.switched && this.status.page === wk_page) {
onPage();
} else if (this.status.switched) {
offPage();
}
this.status = new PageStatus(this.status);
});
this.observer.observe(document, {
childList: true,
subtree: true
});
}
}
let kanji_json = JSON.parse(GM_getResourceText("kanjiJSON"));
class ReviewPage {
constructor() {
this.kanji_elem = null;
this.kanji = null;
this.writer = null;
this.container_div = null;
}
/**
* Called whenever the kanji has switched. It creates the hanzi writer instance
*/
drawHanziWriter() {
if (!this.writer) {
let character_header = document.querySelector(".quiz .character-header");
this.container_div = document.createElement("div");
this.container_div.id = "wkhwa-container-div";
this.writer = HanziWriter.create(this.container_div, this.kanji, {
showOutline: wkof.settings.wkhwa.showOutline,
showCharacter: wkof.settings.wkhwa.showCharacter,
width: wkof.settings.wkhwa.width,
height: wkof.settings.wkhwa.height,
padding: wkof.settings.wkhwa.padding,
strokeAnimationSpeed: wkof.settings.wkhwa.strokeAnimationSpeed,
strokeHighlightSpeed: wkof.settings.wkhwa.strokeHighlightSpeed,
strokeFadeDuration: wkof.settings.wkhwa.strokeFadeDuration,
delayBetweenStrokes: wkof.settings.wkhwa.delayBetweenStrokes,
delayBetweenLoops: wkof.settings.wkhwa.delayBetweenLoops,
strokeColor: wkof.settings.wkhwa.strokeColor,
highlightColor: wkof.settings.wkhwa.highlightColor,
outlineColor: wkof.settings.wkhwa.outlineColor,
drawingColor: wkof.settings.wkhwa.drawingColor,
drawingWidth: wkof.settings.wkhwa.drawingWidth,
showHintAfterMisses: wkof.settings.wkhwa.showHintAfterMisses,
quizStartStrokeNum: wkof.settings.wkhwa.quizStartStrokeNum,
highlightOnComplete: wkof.settings.wkhwa.highlightOnComplete,
charDataLoader: (char, on_load) => {
on_load(kanji_json[char]);
}
});
character_header.append(this.container_div);
} else {
this.writer.setCharacter(this.kanji);
}
if (wkof.settings.wkhwa.quiz) {
this.writer.quiz();
} else if (wkof.settings.wkhwa.animate) {
if (wkof.settings.wkhwa.loop_animation) {
this.writer.loopCharacterAnimation();
} else {
this.writer.animateCharacter();
}
}
}
onReviewPage() {
this.observer = new MutationObserver(() => {
if (this.refreshKanjiState()) {
this.drawHanziWriter();
}
});
this.kanji_elem = document.querySelector(".quiz .character-header .character-header__characters");
if (this.refreshKanjiState()) {
this.drawHanziWriter();
}
this.observer.observe(this.kanji_elem, {
childList: true,
subtree: true
});
}
showHanziWriter() {
this.kanji_elem.hidden = true;
if (this.container_div) {
this.container_div.hidden = false;
}
}
hideHanziWriter() {
this.kanji_elem.hidden = false;
if (this.container_div) {
this.container_div.hidden = true;
}
}
/**
* Returns true if the kanji shown has switched. Returns false otherwise
* This function also manages hiding and showing kanji
* in the event that the characters shown are either a radical or vocabulary
*/
refreshKanjiState() {
// CHANGEME shouldn't this return an enum and have the logic outside this function?
if (document.querySelector(".quiz-input__question-category").innerText.toLowerCase() === "kanji" && kanji_json[this.kanji_elem.innerText]) {
if (this.kanji_elem.innerText !== this.kanji) {
// We have switched to a new kanji, mayhap away from vocabulary, so we need to set these to be shown
this.kanji = this.kanji_elem.innerText;
this.showHanziWriter();
return true;
} else {
return false;
}
}
// The character content has switched to vocabulary or a radical
this.kanji = null;
this.hideHanziWriter();
return false;
}
/**
* Cleans up various objects if we switch away from them.
*/
offReviewPage() {
this.kanji = null;
this.writer = null;
this.container_div = null;
if (this.observer) {
this.observer.disconnect();
}
}
}
// Loads styles
GM_addStyle(`#wkhwa-container-div {
position: relative;
top: -32px;
display: flex;
align-items: center;
justify-content: center;
}`);
let default_settings = {
showOutline: true,
showCharacter: true,
width: 200,
height: 200,
padding: 20,
strokeAnimationSpeed: 1,
strokeHighlightSpeed: 2,
strokeFadeDuration: 400,
delayBetweenStrokes: 1000,
delayBetweenLoops: 2000,
strokeColor: "#555555",
highlightColor: "#8899FF",
outlineColor: "#FFFFFF",
drawingColor: "#333333",
drawingWidth: 20,
showHintAfterMisses: 3,
quizStartStrokeNum: 0,
highlightOnComplete: false,
quiz: true,
animate: false,
loop_animation: false
};
let config = {
script_id: "wkhwa",
title: "Hanzi writer addition",
content: {
hanzi_writer: {
type: "group",
label: "Options:",
content: {
showOutline: {
type: 'checkbox',
label: 'showOutline',
default: default_settings.showOutline
},
showCharacter: {
type: 'colorbox',
label: 'showCharacter',
default: default_settings.showCharacter
},
width: {
type: "number",
label: "width",
default: default_settings.width
},
height: {
type: "number",
label: "height",
default: default_settings.height
},
padding: {
type: "number",
label: "padding",
default: default_settings.padding
},
strokeAnimationSpeed: {
type: "number",
label: "strokeAnimationSpeed",
default: default_settings.strokeAnimationSpeed
},
strokeHighlightSpeed: {
type: "number",
label: "strokeHighlightSpeed",
default: default_settings.strokeHighlightSpeed
},
strokeFadeDuration: {
type: "number",
label: "strokeFadeDuration",
default: default_settings.strokeFadeDuration
},
delayBetweenStrokes: {
type: "number",
label: "delayBetweenStrokes",
default: default_settings.delayBetweenStrokes
},
delayBetweenLoops: {
type: "number",
label: "delayBetweenLoops",
default: default_settings.delayBetweenLoops
},
StrokeColor: {
type: 'color',
label: 'strokeColor',
default: '#555555'
},
highlightColor: {
type: 'color',
label: 'highlightColor',
default: '#8899FF'
},
outlineColor: {
type: 'color',
label: 'outlineColor',
default: '#FFFFFF'
},
drawingColor: {
type: 'color',
label: 'drawingColor',
default: '#333333'
},
drawingWidth: {
type: "number",
label: "drawingWidth",
default: default_settings.drawingWidth
},
showHintAfterMisses: {
type: "number",
label: "showHintAfterMisses",
default: default_settings.showHintAfterMisses
},
quizStartStrokeNum: {
type: "number",
label: "quizStartStrokeNum",
default: default_settings.quizStartStrokeNum
},
highlightOnComplete: {
type: 'checkbox',
label: 'highlightOnComplete',
default: default_settings.highlightOnComplete
},
quiz: {
type: 'checkbox',
label: 'quiz',
default: default_settings.quiz
},
animate: {
type: 'checkbox',
label: 'animate',
default: default_settings.animate
},
loop_animation: {
type: 'checkbox',
label: 'loopAnimation',
default: default_settings.loop_animation
}
}
}
}
};
wkof.include('Menu, Settings');
wkof.ready('Menu, Settings').then(main);
function main() {
wkof.Settings.load('wkhwa', default_settings);
wkof.Menu.insert_script_link({
name: 'wkhwa',
submenu: 'wkhwa',
title: 'Hanzi writer settings',
on_click: new wkof.Settings(config).open
});
let review_page = new ReviewPage();
new PageObserver(WK_PAGE.REVIEW, review_page.onReviewPage.bind(review_page), review_page.offReviewPage.bind(review_page));
}
})();