Wanikani: Dashboard Apprentice

Displays all your apprentice items on the dashboard

  1. // ==UserScript==
  2. // @name Wanikani: Dashboard Apprentice
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2.4
  5. // @description Displays all your apprentice items on the dashboard
  6. // @author Kumirei
  7. // @match https://www.wanikani.com
  8. // @match https://www.wanikani.com/dashboard*
  9. // @match https://preview.wanikani.com
  10. // @match https://preview.wanikani.com/dashboard*
  11. // @grant none
  12. // ==/UserScript==
  13. /*jshint esversion: 8 */
  14.  
  15. ;(function (wkof, $) {
  16. // Make sure WKOF is installed
  17. let script_id = 'dashboard_apprentice'
  18. if (!wkof) {
  19. var script_name = 'Wanikani: Dashboard Apprentice'
  20. var response = confirm(
  21. script_name +
  22. ' requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.',
  23. )
  24. if (response) {
  25. window.location.href =
  26. 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549'
  27. }
  28. return
  29. }
  30. // Ready to go
  31. else {
  32. wkof.include('Menu,Settings,ItemData')
  33. wkof.ready('Menu,Settings,ItemData')
  34. .then(load_settings)
  35. .then(install_menu)
  36. .then(add_css)
  37. .then(fetch_items)
  38. .then(display)
  39. }
  40.  
  41. function install_menu() {
  42. let config = {
  43. name: script_id,
  44. submenu: 'Settings',
  45. title: 'Dashboard Apprentice',
  46. on_click: open_settings,
  47. }
  48. wkof.Menu.insert_script_link(config)
  49. }
  50. function open_settings() {
  51. var config = {
  52. script_id: script_id,
  53. title: 'Dashboard Apprentice',
  54. content: {
  55. theme: {
  56. type: 'dropdown',
  57. label: 'Theme',
  58. default: 0,
  59. hover_tip: 'Changes the colors of the items',
  60. content: { 0: 'Default', 1: 'Breeze Dark' },
  61. },
  62. srs_start: {
  63. type: 'number',
  64. label: 'First SRS stage',
  65. default: 1,
  66. hover_tip:
  67. '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',
  68. },
  69. srs_end: {
  70. type: 'number',
  71. label: 'Last SRS stage',
  72. default: 4,
  73. hover_tip:
  74. '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',
  75. },
  76. types: {
  77. type: 'list',
  78. label: 'Item types',
  79. multi: true,
  80. hover_tip: 'Which items you want to display',
  81. default: { rad: true, kan: true, voc: true },
  82. content: { rad: 'Radicals', kan: 'Kanji', voc: 'Vocabulary', kana_voc: 'Kana Vocabulary' },
  83. },
  84. },
  85. }
  86. let dialog = new wkof.Settings(config)
  87. dialog.open()
  88. }
  89.  
  90. function load_settings() {
  91. let defaults = {
  92. theme: 0,
  93. srs_start: 1,
  94. srs_end: 4,
  95. types: { rad: true, kan: true, voc: true, kana_voc: true },
  96. }
  97. return wkof.Settings.load(script_id, defaults)
  98. }
  99.  
  100. // Fetches the items
  101. async function fetch_items() {
  102. let types = Object.entries(wkof.settings[script_id].types)
  103. .filter((a) => a[1])
  104. .map((a) => a[0])
  105. return wkof.ItemData.get_index(
  106. await wkof.ItemData.get_items({
  107. wk_items: { options: { assignments: true }, filters: { item_type: types } },
  108. }),
  109. 'srs_stage',
  110. )
  111. }
  112.  
  113. // Puts the information on the dashboard
  114. async function display(data) {
  115. let names = {
  116. '-1': 'Locked',
  117. 0: 'Lessons',
  118. 1: 'Apprentice 1',
  119. 2: 'Apprentice 2',
  120. 3: 'Apprentice 3',
  121. 4: 'Apprentice 4',
  122. 5: 'Guru 1',
  123. 6: 'Guru 2',
  124. 7: 'Master',
  125. 8: 'Enlightened',
  126. 9: 'Burned',
  127. }
  128. var elem = $('<section id="wkda_items"></section>')[0]
  129. if (is_dark_theme()) elem.className = 'dark'
  130. let settings = wkof.settings[script_id]
  131. for (var i = settings.srs_start; i <= settings.srs_end; i++) {
  132. if (!data[i]) continue
  133. var srs_elem = $('<div class="apprentice_' + i + '"></div>')[0]
  134. var title = $('<span>' + names[i] + ' </span>')[0]
  135. var items = $('<div class="items"></div>')[0]
  136. srs_elem.appendChild(title)
  137. srs_elem.appendChild(items)
  138. for (var j = 0; j < data[i].length; j++) {
  139. var item = data[i][j]
  140. var info = {
  141. type: item.object,
  142. characters:
  143. item.data.characters !== null
  144. ? item.data.characters
  145. : await wkof.load_file(
  146. item.data.character_images.find((c) => c.content_type === 'image/svg+xml').url,
  147. true,
  148. ),
  149. meanings: [],
  150. readings: [],
  151. level: item.data.level,
  152. url: item.data.document_url,
  153. available:
  154. i == -1
  155. ? 'Locked'
  156. : i == 0
  157. ? 'In lesson queue'
  158. : item.assignments.srs_stage == 9
  159. ? 'Burned'
  160. : Date.parse(item.assignments.available_at) < Date.now()
  161. ? 'Now'
  162. : s_to_dhm((Date.parse(item.assignments.available_at) - Date.now()) / 1000),
  163. }
  164. for (let k = 0; k < item.data.meanings.length; k++) {
  165. info.meanings.push(item.data.meanings[k].meaning)
  166. }
  167. if (item.data.readings) {
  168. for (let k = 0; k < item.data.readings.length; k++) {
  169. info.readings.push(item.data.readings[k].reading)
  170. }
  171. }
  172. var item_elem = $(
  173. '<div class="item ' +
  174. info.type +
  175. '"' +
  176. '>' +
  177. '<div class="hover_elem">' +
  178. '<div class="left">' +
  179. '<a class="' +
  180. info.type +
  181. '" href="' +
  182. info.url +
  183. '">' +
  184. info.characters +
  185. '</a>' +
  186. '</div>' +
  187. '<div class="right">' +
  188. '<table>' +
  189. '<tr><td>Meanings</td><td>' +
  190. info.meanings.join(', ') +
  191. '</td></tr>' +
  192. '<tr><td>Readings</td><td>' +
  193. info.readings.join('、') +
  194. '</td></tr>' +
  195. '<tr><td>Level</td><td>' +
  196. info.level +
  197. '</td></tr>' +
  198. '<tr><td>Available</td><td>' +
  199. info.available +
  200. '</td></tr>' +
  201. '</table>' +
  202. '</div>' +
  203. '</div>' +
  204. '<a class="' +
  205. info.type +
  206. '" href="' +
  207. info.url +
  208. '">' +
  209. info.characters +
  210. '</a>' +
  211. '</div>',
  212. )[0]
  213. items.appendChild(item_elem)
  214. }
  215. elem.appendChild(srs_elem)
  216. }
  217. let target = document.querySelector('.span12 > .row')
  218. target.parentElement.insertBefore(elem, target)
  219. }
  220.  
  221. // Adds the CSS to the page
  222. function add_css() {
  223. let theme = wkof.settings[script_id].theme
  224. $('head').append(
  225. `<style id="wkda_css">
  226. #wkda_items {
  227. background-color: #f4f4f4;
  228. border-radius: 5px;
  229. padding: 16px 24px 12px;
  230. --color-text: ${['rgb(240, 240, 240)', 'black'][theme]} !important;
  231. }
  232. #wkda_items.dark {
  233. background-color: #232629;
  234. }
  235. #wkda_items > div {
  236. margin-bottom: 10px;
  237. }
  238. #wkda_items {
  239. font-size: 16px;
  240. }
  241. #wkda_items .items {
  242. position: relative;
  243. display: flex;
  244. flex-direction: row;
  245. flex-wrap: wrap;
  246. justify-content: flex-start;
  247. margin-left: -2px;
  248. }
  249. #wkda_items .items .item {
  250. display: inline-block;
  251. padding: 0 3px;
  252. margin: 1.5px;
  253. border-radius: 3px;
  254. position: relative;
  255. }
  256. #wkda_items .items .radical {
  257. background: ${['#0096e7', '#3daee9'][theme]};
  258. order: 0;
  259. width: 14px;
  260. }
  261. #wkda_items .items .kanji {
  262. background: ${['#ff00aa', '#fdbc4b'][theme]};
  263. order: 1;
  264. }
  265. #wkda_items .items .vocabulary {
  266. background: ${['#9800e8', '#2ecc71'][theme]};
  267. order: 3;
  268. }
  269. #wkda_items .items .kana_vocabulary {
  270. background: ${['#9800e8', '#2ecc71'][theme]};
  271. order: 2;
  272. }
  273. #wkda_items .hover_elem {
  274. visibility: hidden;
  275. position: absolute;
  276. background-color: rgba(0, 0, 0, 0.9);
  277. z-index: 2;
  278. padding: 5px;
  279. border-radius: 3px;
  280. width: max-content;
  281. transform: translate(-50%, calc(0px - 100% - 5px));
  282. left: 50%;
  283. }
  284. #wkda_items .item:hover .hover_elem {
  285. visibility: visible;
  286. }
  287. #wkda_items .hover_elem::after {
  288. visibility: hidden;
  289. position: absolute;
  290. width: 0;
  291. border-top: 5px solid rgba(0, 0, 0, 0.9);
  292. border-right: 5px solid transparent;
  293. border-left: 5px solid transparent;
  294. content: " ";
  295. font-size: 0;
  296. line-height: 0;
  297. left: 50%;
  298. bottom: -5px;
  299. transform: translateX(-50%);
  300. }
  301. #wkda_items .item:hover .hover_elem::after {
  302. visibility: visible;
  303. }
  304. #wkda_items .hover_elem > div {
  305. display: inline-block;
  306. }
  307. #wkda_items .item.vocabulary .hover_elem > div {
  308. display: block;
  309. }
  310. #wkda_items .left {
  311. vertical-align: top;
  312. }
  313. #wkda_items .item.vocabulary .hover_elem .left {
  314. margin-bottom: 5px;
  315. }
  316. #wkda_items .left a {
  317. font-size: 74px;
  318. line-height: 73px;
  319. min-width: 73px;
  320. display: block;
  321. padding: 5px;
  322. border-radius: 3px;
  323. margin: 3px 10px 0 3px;
  324. }
  325. #wkda_items .item.vocabulary .left a {
  326. margin-right: 3px;
  327. text-align: center;
  328. }
  329. #wkda_items .items .radical svg {
  330. height: 14px;
  331. stroke: currentColor;
  332. fill: none;
  333. stroke-linecap: square;
  334. stroke-width: 68;
  335. }
  336. #wkda_items .items .radical svg g {
  337. clip-path: none;
  338. }
  339. #wkda_items .items .radical .hover_elem svg {
  340. height: 74px;
  341. width: 1em;
  342. }
  343. #wkda_items .right table td:first-child {
  344. padding-right: 10px;
  345. font-weight: bold;
  346. }
  347. #wkda_items .items table td {
  348. color: rgb(240, 240, 240);
  349. }
  350. #wkda_items .items > div a {
  351. color: ${['rgb(240, 240, 240)', 'black'][theme]} !important;
  352. }
  353. #wkda_items .item.vocabulary .hover_elem {
  354. max-width: 320px;
  355. }
  356. </style>`,
  357. )
  358. }
  359.  
  360. // Converts seconds to days, hours, and minutes
  361. function s_to_dhm(s) {
  362. var d = Math.floor(s / 60 / 60 / 24)
  363. var h = Math.floor((s % (60 * 60 * 24)) / 60 / 60)
  364. var m = Math.ceil(((s % (60 * 60 * 24)) % (60 * 60)) / 60)
  365. return (d > 0 ? d + 'd ' : '') + (h > 0 ? h + 'h ' : '') + (m > 0 ? m + 'm' : '1m')
  366. }
  367.  
  368. // Returns a promise and a resolve function
  369. function new_promise() {
  370. var resolve,
  371. promise = new Promise((res, rej) => {
  372. resolve = res
  373. })
  374. return [promise, resolve]
  375. }
  376.  
  377. // Handy little function that rfindley wrote. Checks whether the theme is dark.
  378. function is_dark_theme() {
  379. // Grab the <html> background color, average the RGB. If less than 50% bright, it's dark theme.
  380. return (
  381. $('body')
  382. .css('background-color')
  383. .match(/\((.*)\)/)[1]
  384. .split(',')
  385. .slice(0, 3)
  386. .map((str) => Number(str))
  387. .reduce((a, i) => a + i) /
  388. (255 * 3) <
  389. 0.5
  390. )
  391. }
  392. })(window.wkof, window.$)