Wanikani: Levels by SRS

Displays your level for each SRS stage.

  1. // ==UserScript==
  2. // @name Wanikani: Levels by SRS
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.7
  5. // @description Displays your level for each SRS stage.
  6. // @author Kumirei
  7. // @match https://www.wanikani.com/dashboard
  8. // @match https://www.wanikani.com
  9. // @include *preview.wanikani.com*
  10. // @grant none
  11. // ==/UserScript==
  12. /*jshint esversion: 8 */
  13.  
  14. ;(function () {
  15. //check that the Wanikani Framework is installed
  16. var script_name = 'Levels By SRS'
  17. if (!window.wkof) {
  18. if (
  19. confirm(
  20. script_name +
  21. ' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?',
  22. )
  23. )
  24. window.location.href =
  25. 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549'
  26. return
  27. }
  28. //if it's installed then do the stuffs
  29. else {
  30. wkof.include('Menu,Settings,ItemData')
  31. wkof.ready('Menu,Settings,ItemData').then(load_settings).then(install_menu).then(add_css).then(fetch_and_update)
  32. }
  33.  
  34. // Fetches items and updates display
  35. function fetch_and_update() {
  36. fetch_items().then(process_items).then(update_display)
  37. }
  38.  
  39. // Fetches the relevant items
  40. function fetch_items() {
  41. var [promise, resolve] = new_promise()
  42. var config = {
  43. wk_items: {
  44. options: { assignments: true },
  45. filters: { level: '1..+0' },
  46. },
  47. }
  48. wkof.ItemData.get_items(config).then(resolve)
  49. return promise
  50. }
  51.  
  52. // Retreives the levels by srs
  53. function process_items(data) {
  54. // Sort by level
  55. var levels = {}
  56. let item
  57. for (var i = 0; i < data.length; i++) {
  58. item = data[i]
  59. var level = item.data.level
  60. if (!levels[level]) levels[level] = []
  61. levels[level].push(item)
  62. }
  63. // Go through items level by level
  64. var srs_levels = {
  65. Ini: [0, 0, 0],
  66. App: [0, 0, 0],
  67. Gur: [0, 0, 0],
  68. Mas: [0, 0, 0],
  69. Enl: [0, 0, 0],
  70. Bur: [0, 0, 0],
  71. }
  72. for (i = 1; i < wkof.user.level + 1; i++) {
  73. // Get counts for level
  74. var by_srs = { Ini: 0, App: 0, Gur: 0, Mas: 0, Enl: 0, Bur: 0, total: 0 }
  75. for (var j = 0; j < levels[i].length; j++) {
  76. item = levels[i][j]
  77. if (item.assignments)
  78. by_srs[
  79. ['Ini', 'App', 'App', 'App', 'App', 'Gur', 'Gur', 'Mas', 'Enl', 'Bur'][
  80. item.assignments.srs_stage
  81. ]
  82. ]++
  83. else by_srs.Ini++
  84. by_srs.total++
  85. }
  86. // Check if srs_level should be increased
  87. var types = ['Ini', 'App', 'Gur', 'Mas', 'Enl', 'Bur']
  88. var cumulative = 0
  89. for (j = 0; j < types.length; j++) {
  90. var count = by_srs[types[j]]
  91. if (
  92. 1 - cumulative / by_srs.total >= wkof.settings.levels_by_srs.threshold / 100 &&
  93. i == srs_levels[types[j]][0] + 1
  94. ) {
  95. srs_levels[types[j]][0]++
  96. } else if (
  97. 1 - cumulative / by_srs.total <= wkof.settings.levels_by_srs.threshold / 100 &&
  98. i == srs_levels[types[j]][0] + 1
  99. ) {
  100. srs_levels[types[j]][1] = 1 - cumulative / by_srs.total
  101. srs_levels[types[j]][2] = by_srs.total
  102. }
  103. cumulative += count
  104. }
  105. }
  106. return srs_levels
  107. }
  108.  
  109. // Updates the display element
  110. function update_display(srs_levels) {
  111. var types = ['App', 'Gur', 'Mas', 'Enl', 'Bur']
  112. // If the element doesn't already exist, create it
  113. var display = $('#levels_by_srs')
  114. if (!display.length) {
  115. display = $('<div id="levels_by_srs"' + (is_dark_theme() ? ' class="dark_theme"' : '') + '></div>')
  116. for (var i = 0; i < types.length; i++)
  117. display.append(
  118. '<div class="' +
  119. types[i] +
  120. '"><span class="level_label">Level: </span><span class="value"></span></div>',
  121. )
  122. $('.srs-progress').append(display)
  123. }
  124. // Update
  125. for (let i = 0; i < types.length; i++) {
  126. var elem = $(display).find('.' + types[i] + ' span.value')[0]
  127. elem.innerText = srs_levels[types[i]][0]
  128. elem.parentElement.setAttribute(
  129. 'title',
  130. Math.floor(srs_levels[types[i]][1] * 100) +
  131. '% to level ' +
  132. (srs_levels[types[i]][0] + 1) +
  133. ' (' +
  134. Math.round(srs_levels[types[i]][1] * srs_levels[types[i]][2]) +
  135. ' of ' +
  136. srs_levels[types[i]][2] +
  137. ')',
  138. )
  139. }
  140. }
  141.  
  142. // Load stored settings or set defaults
  143. function load_settings() {
  144. var defaults = { threshold: 90 }
  145. return wkof.Settings.load('levels_by_srs', defaults)
  146. }
  147.  
  148. // Installs the options button in the menu
  149. function install_menu() {
  150. var config = {
  151. name: 'levels_by_srs',
  152. submenu: 'Settings',
  153. title: 'Levels By SRS',
  154. on_click: open_settings,
  155. }
  156. wkof.Menu.insert_script_link(config)
  157. }
  158.  
  159. // Create the options
  160. function open_settings(items) {
  161. var config = {
  162. script_id: 'levels_by_srs',
  163. title: 'Levels By SRS',
  164. on_save: fetch_and_update,
  165. content: {
  166. threshold: {
  167. type: 'number',
  168. label: 'Threshold',
  169. hover_tip: 'Percentage to consider level done',
  170. default: 90,
  171. },
  172. },
  173. }
  174. var dialog = new wkof.Settings(config)
  175. dialog.open()
  176. }
  177.  
  178. // Adds the script's CSS to the page
  179. function add_css() {
  180. $('head').append(`<style id="levels_by_srs_CSS">
  181. #levels_by_srs {
  182. background: #434343;
  183. border-radius: 0 0 3px 3px;
  184. height: 30px;
  185. line-height: 30px;
  186. color: rgb(240, 240, 240);
  187. display: flex;
  188. justify-content: space-around;
  189. }
  190. #levels_by_srs > div {
  191. display: flex;
  192. flex: 1;
  193. justify-content: center;
  194. }
  195. #levels_by_srs .level_label {
  196. font-size: 16px;
  197. margin-right: 5px;
  198. }
  199. #levels_by_srs .value {
  200. font-size: 16px;
  201. font-weight: normal;
  202. }
  203. #levels_by_srs.dark_theme {
  204. background: #232629;
  205. }
  206. #levels_by_srs.dark_theme > div:not(:last-child) {
  207. border-right: 1px solid #31363b;
  208. margin-right: 5px;
  209. }
  210. #levels_by_srs.dark_theme > div:last-child {
  211. border-right: 1px solid transparent;
  212. }
  213. .srs-progress > ul > li {
  214. border-radius: 0 !important;
  215. }
  216. </style>`)
  217. }
  218.  
  219. // Returns a promise and a resolve function
  220. function new_promise() {
  221. var resolve,
  222. promise = new Promise((res, rej) => {
  223. resolve = res
  224. })
  225. return [promise, resolve]
  226. }
  227.  
  228. // Handy little function that rfindley wrote. Checks whether the theme is dark.
  229. function is_dark_theme() {
  230. // Grab the <html> background color, average the RGB. If less than 50% bright, it's dark theme.
  231. return (
  232. $('body')
  233. .css('background-color')
  234. .match(/\((.*)\)/)[1]
  235. .split(',')
  236. .slice(0, 3)
  237. .map((str) => Number(str))
  238. .reduce((a, i) => a + i) /
  239. (255 * 3) <
  240. 0.5
  241. )
  242. }
  243. })()