Wanikani hanzi-writer addition

Replaces kanji in wanikani with hanzi writer. Licenses for kanji data are found at https://github.com/chanind/hanzi-writer-data-jp/

  1. // ==UserScript==
  2. // @name Wanikani hanzi-writer addition
  3. // @namespace https://declanfodor.com
  4. // @description Replaces kanji in wanikani with hanzi writer. Licenses for kanji data are found at https://github.com/chanind/hanzi-writer-data-jp/
  5. // @match https://www.wanikani.com/*
  6. // @version 0.0.4
  7. // @author Declan Fodor
  8. // @resource kanjiJSON https://raw.githubusercontent.com/chanind/hanzi-writer-data-jp/master/data/all.json
  9. // @require https://cdn.jsdelivr.net/npm/hanzi-writer@3.5/dist/hanzi-writer.js
  10. // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2
  11. // @license MIT
  12. // @grant GM_addStyle
  13. // @grant GM_getResourceText
  14. // @grant GM_getValue
  15. // @grant GM_setvalue
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. 'use strict';
  20.  
  21. const WK_PAGE = Object.freeze({
  22. REVIEW: Symbol("review_page"),
  23. LESSON: Symbol("lesson_page"),
  24. DASHBOARD: Symbol("dashboard_page"),
  25. LOADING: Symbol("other_page") // Or a page we haven't implemented behavior for yet
  26. });
  27. class PageStatus {
  28. constructor(previousStatus) {
  29. this.page = this.whichPage(unsafeWindow.location.href);
  30. this.switched = previousStatus ? previousStatus.page !== this.page : true;
  31. }
  32. whichPage(url) {
  33. switch (url) {
  34. case "https://www.wanikani.com/":
  35. return WK_PAGE.DASHBOARD;
  36. case "https://www.wanikani.com/subjects/review":
  37. return WK_PAGE.REVIEW;
  38. default:
  39. return WK_PAGE.LOADING;
  40. }
  41. }
  42. }
  43.  
  44. class PageObserver {
  45. constructor(wk_page, onPage, offPage) {
  46. this.status = new PageStatus(null);
  47. this.observer = new MutationObserver(() => {
  48. if (this.status.switched && this.status.page === wk_page) {
  49. onPage();
  50. } else if (this.status.switched) {
  51. offPage();
  52. }
  53. this.status = new PageStatus(this.status);
  54. });
  55. this.observer.observe(document, {
  56. childList: true,
  57. subtree: true
  58. });
  59. }
  60. }
  61.  
  62. let kanji_json = JSON.parse(GM_getResourceText("kanjiJSON"));
  63. class ReviewPage {
  64. constructor() {
  65. this.kanji_elem = null;
  66. this.kanji = null;
  67. this.writer = null;
  68. this.container_div = null;
  69. }
  70. /**
  71. * Called whenever the kanji has switched. It creates the hanzi writer instance
  72. */
  73. drawHanziWriter() {
  74. if (!this.writer) {
  75. let character_header = document.querySelector(".quiz .character-header");
  76. this.container_div = document.createElement("div");
  77. this.container_div.id = "wkhwa-container-div";
  78. this.writer = HanziWriter.create(this.container_div, this.kanji, {
  79. showOutline: wkof.settings.wkhwa.showOutline,
  80. showCharacter: wkof.settings.wkhwa.showCharacter,
  81. width: wkof.settings.wkhwa.width,
  82. height: wkof.settings.wkhwa.height,
  83. padding: wkof.settings.wkhwa.padding,
  84. strokeAnimationSpeed: wkof.settings.wkhwa.strokeAnimationSpeed,
  85. strokeHighlightSpeed: wkof.settings.wkhwa.strokeHighlightSpeed,
  86. strokeFadeDuration: wkof.settings.wkhwa.strokeFadeDuration,
  87. delayBetweenStrokes: wkof.settings.wkhwa.delayBetweenStrokes,
  88. delayBetweenLoops: wkof.settings.wkhwa.delayBetweenLoops,
  89. strokeColor: wkof.settings.wkhwa.strokeColor,
  90. highlightColor: wkof.settings.wkhwa.highlightColor,
  91. outlineColor: wkof.settings.wkhwa.outlineColor,
  92. drawingColor: wkof.settings.wkhwa.drawingColor,
  93. drawingWidth: wkof.settings.wkhwa.drawingWidth,
  94. showHintAfterMisses: wkof.settings.wkhwa.showHintAfterMisses,
  95. quizStartStrokeNum: wkof.settings.wkhwa.quizStartStrokeNum,
  96. highlightOnComplete: wkof.settings.wkhwa.highlightOnComplete,
  97. charDataLoader: (char, on_load) => {
  98. on_load(kanji_json[char]);
  99. }
  100. });
  101. character_header.append(this.container_div);
  102. } else {
  103. this.writer.setCharacter(this.kanji);
  104. }
  105. if (wkof.settings.wkhwa.quiz) {
  106. this.writer.quiz();
  107. } else if (wkof.settings.wkhwa.animate) {
  108. if (wkof.settings.wkhwa.loop_animation) {
  109. this.writer.loopCharacterAnimation();
  110. } else {
  111. this.writer.animateCharacter();
  112. }
  113. }
  114. }
  115. onReviewPage() {
  116. this.observer = new MutationObserver(() => {
  117. if (this.refreshKanjiState()) {
  118. this.drawHanziWriter();
  119. }
  120. });
  121. this.kanji_elem = document.querySelector(".quiz .character-header .character-header__characters");
  122. if (this.refreshKanjiState()) {
  123. this.drawHanziWriter();
  124. }
  125. this.observer.observe(this.kanji_elem, {
  126. childList: true,
  127. subtree: true
  128. });
  129. }
  130. showHanziWriter() {
  131. this.kanji_elem.hidden = true;
  132. if (this.container_div) {
  133. this.container_div.hidden = false;
  134. }
  135. }
  136. hideHanziWriter() {
  137. this.kanji_elem.hidden = false;
  138. if (this.container_div) {
  139. this.container_div.hidden = true;
  140. }
  141. }
  142. /**
  143. * Returns true if the kanji shown has switched. Returns false otherwise
  144. * This function also manages hiding and showing kanji
  145. * in the event that the characters shown are either a radical or vocabulary
  146. */
  147. refreshKanjiState() {
  148. // CHANGEME shouldn't this return an enum and have the logic outside this function?
  149. if (document.querySelector(".quiz-input__question-category").innerText.toLowerCase() === "kanji" && kanji_json[this.kanji_elem.innerText]) {
  150. if (this.kanji_elem.innerText !== this.kanji) {
  151. // We have switched to a new kanji, mayhap away from vocabulary, so we need to set these to be shown
  152. this.kanji = this.kanji_elem.innerText;
  153. this.showHanziWriter();
  154. return true;
  155. } else {
  156. return false;
  157. }
  158. }
  159. // The character content has switched to vocabulary or a radical
  160. this.kanji = null;
  161. this.hideHanziWriter();
  162. return false;
  163. }
  164. /**
  165. * Cleans up various objects if we switch away from them.
  166. */
  167. offReviewPage() {
  168. this.kanji = null;
  169. this.writer = null;
  170. this.container_div = null;
  171. if (this.observer) {
  172. this.observer.disconnect();
  173. }
  174. }
  175. }
  176.  
  177. // Loads styles
  178. GM_addStyle(`#wkhwa-container-div {
  179. position: relative;
  180. top: -32px;
  181. display: flex;
  182. align-items: center;
  183. justify-content: center;
  184. }`);
  185. let default_settings = {
  186. showOutline: true,
  187. showCharacter: true,
  188. width: 200,
  189. height: 200,
  190. padding: 20,
  191. strokeAnimationSpeed: 1,
  192. strokeHighlightSpeed: 2,
  193. strokeFadeDuration: 400,
  194. delayBetweenStrokes: 1000,
  195. delayBetweenLoops: 2000,
  196. strokeColor: "#555555",
  197. highlightColor: "#8899FF",
  198. outlineColor: "#FFFFFF",
  199. drawingColor: "#333333",
  200. drawingWidth: 20,
  201. showHintAfterMisses: 3,
  202. quizStartStrokeNum: 0,
  203. highlightOnComplete: false,
  204. quiz: true,
  205. animate: false,
  206. loop_animation: false
  207. };
  208. let config = {
  209. script_id: "wkhwa",
  210. title: "Hanzi writer addition",
  211. content: {
  212. hanzi_writer: {
  213. type: "group",
  214. label: "Options:",
  215. content: {
  216. showOutline: {
  217. type: 'checkbox',
  218. label: 'showOutline',
  219. default: default_settings.showOutline
  220. },
  221. showCharacter: {
  222. type: 'colorbox',
  223. label: 'showCharacter',
  224. default: default_settings.showCharacter
  225. },
  226. width: {
  227. type: "number",
  228. label: "width",
  229. default: default_settings.width
  230. },
  231. height: {
  232. type: "number",
  233. label: "height",
  234. default: default_settings.height
  235. },
  236. padding: {
  237. type: "number",
  238. label: "padding",
  239. default: default_settings.padding
  240. },
  241. strokeAnimationSpeed: {
  242. type: "number",
  243. label: "strokeAnimationSpeed",
  244. default: default_settings.strokeAnimationSpeed
  245. },
  246. strokeHighlightSpeed: {
  247. type: "number",
  248. label: "strokeHighlightSpeed",
  249. default: default_settings.strokeHighlightSpeed
  250. },
  251. strokeFadeDuration: {
  252. type: "number",
  253. label: "strokeFadeDuration",
  254. default: default_settings.strokeFadeDuration
  255. },
  256. delayBetweenStrokes: {
  257. type: "number",
  258. label: "delayBetweenStrokes",
  259. default: default_settings.delayBetweenStrokes
  260. },
  261. delayBetweenLoops: {
  262. type: "number",
  263. label: "delayBetweenLoops",
  264. default: default_settings.delayBetweenLoops
  265. },
  266. StrokeColor: {
  267. type: 'color',
  268. label: 'strokeColor',
  269. default: '#555555'
  270. },
  271. highlightColor: {
  272. type: 'color',
  273. label: 'highlightColor',
  274. default: '#8899FF'
  275. },
  276. outlineColor: {
  277. type: 'color',
  278. label: 'outlineColor',
  279. default: '#FFFFFF'
  280. },
  281. drawingColor: {
  282. type: 'color',
  283. label: 'drawingColor',
  284. default: '#333333'
  285. },
  286. drawingWidth: {
  287. type: "number",
  288. label: "drawingWidth",
  289. default: default_settings.drawingWidth
  290. },
  291. showHintAfterMisses: {
  292. type: "number",
  293. label: "showHintAfterMisses",
  294. default: default_settings.showHintAfterMisses
  295. },
  296. quizStartStrokeNum: {
  297. type: "number",
  298. label: "quizStartStrokeNum",
  299. default: default_settings.quizStartStrokeNum
  300. },
  301. highlightOnComplete: {
  302. type: 'checkbox',
  303. label: 'highlightOnComplete',
  304. default: default_settings.highlightOnComplete
  305. },
  306. quiz: {
  307. type: 'checkbox',
  308. label: 'quiz',
  309. default: default_settings.quiz
  310. },
  311. animate: {
  312. type: 'checkbox',
  313. label: 'animate',
  314. default: default_settings.animate
  315. },
  316. loop_animation: {
  317. type: 'checkbox',
  318. label: 'loopAnimation',
  319. default: default_settings.loop_animation
  320. }
  321. }
  322. }
  323. }
  324. };
  325. wkof.include('Menu, Settings');
  326. wkof.ready('Menu, Settings').then(main);
  327. function main() {
  328. wkof.Settings.load('wkhwa', default_settings);
  329. wkof.Menu.insert_script_link({
  330. name: 'wkhwa',
  331. submenu: 'wkhwa',
  332. title: 'Hanzi writer settings',
  333. on_click: new wkof.Settings(config).open
  334. });
  335. let review_page = new ReviewPage();
  336. new PageObserver(WK_PAGE.REVIEW, review_page.onReviewPage.bind(review_page), review_page.offReviewPage.bind(review_page));
  337. }
  338.  
  339. })();