Wanikani Dashboard Progress Plus

Display detailed level progress.

  1. // ==UserScript==
  2. // @name Wanikani Dashboard Progress Plus
  3. // @namespace rfindley
  4. // @description Display detailed level progress.
  5. // @version 3.1.8
  6. // @match https://www.wanikani.com/
  7. // @match https://www.wanikani.com/dashboard
  8. // @match https://preview.wanikani.com/
  9. // @match https://preview.wanikani.com/dashboard
  10. // @copyright 2015-2023, Robin Findley
  11. // @license MIT; http://opensource.org/licenses/MIT
  12. // @run-at document-end
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. window.dpp = {};
  17.  
  18. (function(gobj) {
  19.  
  20. /* global $, wkof */
  21.  
  22. //===================================================================
  23. // Initialization of the Wanikani Open Framework.
  24. //-------------------------------------------------------------------
  25. let script_name = 'Dashboard Progress Plus';
  26. if (!window.wkof) {
  27. if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) {
  28. window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
  29. }
  30. return;
  31. }
  32.  
  33. wkof.include('ItemData, Menu, Settings');
  34. wkof.ready('document,ItemData,Menu,Settings').then(load_settings).then(startup);
  35.  
  36. //========================================================================
  37. // Global variables
  38. //-------------------------------------------------------------------
  39. let settings, settings_dialog;
  40.  
  41. //========================================================================
  42. // Load the script settings.
  43. //-------------------------------------------------------------------
  44. function load_settings() {
  45. let defaults = {
  46. visible_items: 'all',
  47. locked_position: 'first',
  48. show_90percent: true,
  49. show_char: true,
  50. show_lock_icon: true,
  51. show_lesson_icon: true,
  52. enable_popup: true,
  53. show_meaning: true,
  54. show_reading: true,
  55. show_srs: true,
  56. show_next_review: true,
  57. show_passed: true,
  58. lesson_order: 'wanikani',
  59. time_format: '12hour',
  60. };
  61. return wkof.Settings.load('dpp', defaults).then(function(data){
  62. settings = wkof.settings.dpp;
  63. });
  64. }
  65.  
  66. //========================================================================
  67. // Open the settings dialog
  68. //-------------------------------------------------------------------
  69. function open_settings() {
  70. let config = {
  71. script_id: 'dpp',
  72. title: 'Dashboard Progress Plus',
  73. on_save: settings_saved,
  74. on_refresh: refresh_settings,
  75. content: {
  76. tabs: {type:'tabset', content: {
  77. pgLayout: {type:'page', label:'Main View', hover_tip:'Settings for the main view.', content: {
  78. visible_items: {type:'dropdown', label:'Visible Items', default:'all', content:{all:'All Items',non_passed:'Non-passed (Apprentice)',passed:'Passed Items (Guru+)'}, hover_tip:'Choose which items to show.'},
  79. show_90percent: {type:'checkbox', label:'Show 90% Bracket', default:true, hover_tip:'Show the bracket around 90% of items.'},
  80. show_char: {type:'checkbox', label:'Show Kanji/Radical', default:true, hover_tip:'Show the kanji or radical inside each tile.'},
  81. show_lock_icon: {type:'checkbox', label:'Show Lock Icon', default:true, hover_tip:'Show a lock icon on locked items.'},
  82. show_lesson_icon: {type:'checkbox', label:'Show Lesson Icon', default:true, hover_tip:'Show an "L" icon on pending lessons.'},
  83. locked_position: {type:'dropdown', label:'Locked Item Position', default:'first', content:{first:'First',last:'Last'}, hover_tip:'Choose where locked items are placed.'},
  84. lesson_order: {type:'dropdown', label:'Lesson Sort Order', default:'wanikani', content:{wanikani:'WaniKani Order',subject:'Subject'}, hover_tip:'Choose the sort order of lesson items.'},
  85. }},
  86. pgPopupInfo: {type:'page', label:'Pop-up Info', hover_tip:'Information shown in the popup box.', content: {
  87. enable_popup: {type:'checkbox', label:'Enable Pop-up Info Box', default:true, refresh_on_change:true, hover_tip:'Choose whether to show pop-up info box when hovering over an item.'},
  88. grpPopupInfo: {type:'group', label:'Pop-up Info', hover_tip:'Information to display in the pop-up box.', content:{
  89. show_meaning: {type:'checkbox', label:'Show Meaning', default:true, hover_tip:'Choose whether to show the item\'s meaning in the pop-up info.'},
  90. show_reading: {type:'checkbox', label:'Show Reading', default:true, hover_tip:'Choose whether to show the item\'s reading in the pop-up info.'},
  91. show_srs: {type:'checkbox', label:'Show SRS Level', default:true, hover_tip:'Choose whether to show the item\'s SRS level in the pop-up info.'},
  92. show_next_review: {type:'checkbox', label:'Show Next Review Date', default:true, hover_tip:'Choose whether to show the item\'s next review date in the pop-up info.'},
  93. show_passed: {type:'checkbox', label:'Show Passed Date', default:true, hover_tip:'Choose whether to show the date that the item passed in the pop-up info.'},
  94. time_format: {type:'dropdown', label:'Time Format', default:'12hour', content:{'12hour':'12-hour','24hour':'24-hour'}, hover_tip:'Display time in 12 or 24-hour format.'},
  95. }}
  96. }}
  97. }}
  98. }
  99. };
  100. let settings_dialog = new wkof.Settings(config);
  101. settings_dialog.open();
  102. }
  103.  
  104. //========================================================================
  105. // Refresh settings dialog
  106. //------------------------------------------------------------------------
  107. function refresh_settings(settings) {
  108. if (settings.enable_popup) {
  109. $('#dpp_show_meaning').prop('disabled', false).closest('.row').removeClass('disabled');
  110. $('#dpp_show_reading').prop('disabled', false).closest('.row').removeClass('disabled');
  111. $('#dpp_show_srs').prop('disabled', false).closest('.row').removeClass('disabled');
  112. $('#dpp_show_next_review').prop('disabled', false).closest('.row').removeClass('disabled');
  113. $('#dpp_show_passed').prop('disabled', false).closest('.row').removeClass('disabled');
  114. $('#dpp_time_format').prop('disabled', false).closest('.row').removeClass('disabled');
  115. } else {
  116. $('#dpp_show_meaning').prop('disabled', true).closest('.row').addClass('disabled');
  117. $('#dpp_show_reading').prop('disabled', true).closest('.row').addClass('disabled');
  118. $('#dpp_show_srs').prop('disabled', true).closest('.row').addClass('disabled');
  119. $('#dpp_show_next_review').prop('disabled', true).closest('.row').addClass('disabled');
  120. $('#dpp_show_passed').prop('disabled', true).closest('.row').addClass('disabled');
  121. $('#dpp_time_format').prop('disabled', true).closest('.row').addClass('disabled');
  122. }
  123. }
  124.  
  125. //========================================================================
  126. // Startup
  127. //-------------------------------------------------------------------
  128. function startup() {
  129. install_css();
  130. install_menu();
  131. init_ui();
  132.  
  133. wkof.ItemData.get_items({
  134. wk_items:{
  135. options:{
  136. assignments:true
  137. },
  138. filters:{
  139. level:'+0',
  140. item_type:'radical,kanji',
  141. }
  142. }
  143. })
  144. .then(process_items);
  145. }
  146.  
  147. //========================================================================
  148. // CSS Styling
  149. //-------------------------------------------------------------------
  150. let progress_css =
  151. '#wkofs_dpp .row.disabled label {opacity:0.5;}'+
  152.  
  153. 'div.dpp-progress {margin-top:0; margin-right:0; padding-right:8px;}'+
  154. '.dpp-progress:not(.pct90) a {margin-top:4px;}'+
  155. '.dpp-progress a {position:relative;}'+
  156. '.dpp-progress.pct90 {background:#fff; border-radius:0; border-color:#777; border-style:solid; border-width:1px 0; padding-top:3px; padding-bottom:2px;}'+
  157. '.dpp-progress.pct90.pct90_left {border-left-width:1px; border-top-left-radius:7px; border-bottom-left-radius:7px; padding-left:3px; margin-left:-4px;}'+
  158. '.dpp-progress.pct90.pct90_right {border-right-width:1px; border-top-right-radius:7px; border-bottom-right-radius:7px; padding-right:3px;}'+
  159. '.level-progress-dashboard__content[data-hide-char="true"] .dpp-progress a {color:transparent; text-shadow:unset;}'+
  160. '.dpp-progress.dpp-noshow {display:none;}'+
  161.  
  162. // Radical colors
  163. '.dpp-progress[data-srs-lvl="-1"] .subject-character__characters {background-repeat:no-repeat;background-image: url("'+
  164. 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACcAAAAnCAYAAACMo1E1AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccll'+
  165. 'PAAAAF5JREFUeNrs07ENwCAMBEDwspkp0zpBQiJN+i+OAqSvXjY3u/se51z7jcgqtdi6KrXYyua71pFY7Du5yH9XqcWAAAIIIIAAAggggAACCCCAAA'+
  166. 'IIIIAAAgggfrNHgAEAXq5IabsNBOwAAAAASUVORK5CYII='+
  167. '");}'+
  168. '.level-progress-dashboard__items[data-lock-icon="y"] .dpp-progress[data-srs-lvl="-1"] .subject-character::before {'+
  169. ' content:"\\f023";font-family:"FontAwesome";font-size:13pt;color:#ff8;position:absolute;left:-4px;top:-6px;-webkit-text-stroke:1px black;}'+
  170. '.level-progress-dashboard__items[data-lesson-icon="y"] .dpp-progress[data-srs-lvl="0"] .subject-character::before {'+
  171. ' content:"L";font-size:12px;font-weight:bold;color:black;position:absolute;left:-4px;top:-6px;border-radius:50%;border:1px solid black;width:14px;height:14px;display:inline-block;padding:0;margin:0;line-height:14px;background-color:#ff8;text-align:center;}'+
  172. '[data-type="radical"] .dpp-progress[data-srs-lvl="-1"] .subject-character__characters {background-color:#00aaff;}'+
  173. '[data-type="radical"] .dpp-progress[data-srs-lvl="0"] .subject-character__characters {background-color:#00aaff; background-image:var(--color-radical-gradient);}'+
  174. '[data-type="radical"] .dpp-progress[data-srs-lvl="1"] .subject-character__characters {background-color:#00aaff; background-image:var(--color-radical-gradient);}'+
  175. '[data-type="radical"] .dpp-progress[data-srs-lvl="2"] .subject-character__characters {background-color:#00aaff; background-image:var(--color-radical-gradient);}'+
  176. '[data-type="radical"] .dpp-progress[data-srs-lvl="3"] .subject-character__characters {background-color:#00aaff; background-image:var(--color-radical-gradient);}'+
  177. '[data-type="radical"] .dpp-progress[data-srs-lvl="4"] .subject-character__characters {background-color:#00aaff; background-image:var(--color-radical-gradient);}'+
  178. '[data-type="radical"] .dpp-progress[data-srs-lvl="5"] .subject-character__characters {background-color:#b69acd; background-image:linear-gradient(0deg,#9065b3,#b69acd);}'+
  179. '[data-type="radical"] .dpp-progress[data-srs-lvl="6"] .subject-character__characters {background-color:#b69acd; background-image:linear-gradient(0deg,#9065b3,#b69acd);}'+
  180. '[data-type="radical"] .dpp-progress[data-srs-lvl="7"] .subject-character__characters {background-color:#9aa5cf; background-image:linear-gradient(0deg,#7483be,#9aa5cf);}'+
  181. '[data-type="radical"] .dpp-progress[data-srs-lvl="8"] .subject-character__characters {background-color:#a3c3d3; background-image:linear-gradient(0deg,#75a5bd,#a3c3d3);}'+
  182. '[data-type="radical"] .dpp-progress[data-srs-lvl="9"] .subject-character__characters {background-color:#999999; background-image:linear-gradient(0deg,#737373,#999999);}'+
  183.  
  184. // Kanji colors
  185. '[data-type="kanji"] .dpp-progress[data-srs-lvl="-1"] .subject-character__characters {background-color:ff00aa;}'+
  186. '[data-type="kanji"] .dpp-progress[data-srs-lvl="0"] .subject-character__characters {background-color:#ff00aa; background-image:var(--color-kanji-gradient);}'+
  187. '[data-type="kanji"] .dpp-progress[data-srs-lvl="1"] .subject-character__characters {background-color:#ff00aa; background-image:var(--color-kanji-gradient);}'+
  188. '[data-type="kanji"] .dpp-progress[data-srs-lvl="2"] .subject-character__characters {background-color:#ff00aa; background-image:var(--color-kanji-gradient);}'+
  189. '[data-type="kanji"] .dpp-progress[data-srs-lvl="3"] .subject-character__characters {background-color:#ff00aa; background-image:var(--color-kanji-gradient);}'+
  190. '[data-type="kanji"] .dpp-progress[data-srs-lvl="4"] .subject-character__characters {background-color:#ff00aa; background-image:var(--color-kanji-gradient);}'+
  191. '[data-type="kanji"] .dpp-progress[data-srs-lvl="5"] .subject-character__characters {background-color:#b69acd; background-image:linear-gradient(0deg,#9065b3,#b69acd);}'+
  192. '[data-type="kanji"] .dpp-progress[data-srs-lvl="6"] .subject-character__characters {background-color:#b69acd; background-image:linear-gradient(0deg,#9065b3,#b69acd);}'+
  193. '[data-type="kanji"] .dpp-progress[data-srs-lvl="7"] .subject-character__characters {background-color:#9aa5cf; background-image:linear-gradient(0deg,#7483be,#9aa5cf);}'+
  194. '[data-type="kanji"] .dpp-progress[data-srs-lvl="8"] .subject-character__characters {background-color:#a3c3d3; background-image:linear-gradient(0deg,#75a5bd,#a3c3d3);}'+
  195. '[data-type="kanji"] .dpp-progress[data-srs-lvl="9"] .subject-character__characters {background-color:#999999; background-image:linear-gradient(0deg,#737373,#999999);}'+
  196.  
  197. '.level-progress-dashboard__items .popover {border-radius:5px; border:5px solid rgba(75,75,75,0.8); box-shadow:none; padding:4px;}'+
  198. '.level-progress-dashboard__content .popover.right .arrow {border-right-color:rgba(75,75,75,0.8); left:-16px;}'+
  199. '.level-progress-dashboard__content .popover.right .arrow:after {border-color:transparent;}'+
  200. '.level-progress-dashboard__content .popover.left .arrow {border-left-color:rgba(75,75,75,0.55);}'+
  201. '.level-progress-dashboard__content .popover .popover-content {text-shadow: 0 1px 0 #fff;}'+
  202. '.level-progress-dashboard__content .popover .srs {font-size:75%; font-weight:bold;}'+
  203. '.level-progress-dashboard__content .popover .next {font-size:75%; font-weight:bold;}';
  204.  
  205. //========================================================================
  206. // Install stylesheet.
  207. //-------------------------------------------------------------------
  208. function install_css() {
  209. $('head').append('<style>'+progress_css+'</style>');
  210. }
  211.  
  212. //========================================================================
  213. // Install menu link
  214. //-------------------------------------------------------------------
  215. function install_menu() {
  216. // Set up menu item to open script.
  217. wkof.Menu.insert_script_link({name:'dpp',submenu:'Settings',title:'Dashboard Progress Plus',on_click:open_settings});
  218. }
  219.  
  220. //========================================================================
  221. // Initialize the user interface.
  222. //-------------------------------------------------------------------
  223. function init_ui() {
  224. $('.level-progress-dashboard__content').attr('data-hide-char', !settings.show_char);
  225. if (settings.enable_popup) {
  226. $('.level-progress-dashboard__items').popover({
  227. selector:'.dpp-progress',
  228. trigger:'hover',
  229. animation: false,
  230. html:true,
  231. content:generate_item_info,
  232. placement:place_item_info,
  233. });
  234. } else {
  235. $('.level-progress-dashboard__items').popover('destroy');
  236. }
  237. $('.level-progress-dashboard__items').attr('data-lock-icon', (settings.show_lock_icon ? 'y' : 'n'));
  238. $('.level-progress-dashboard__items').attr('data-lesson-icon', (settings.show_lesson_icon ? 'y' : 'n'));
  239. }
  240.  
  241. //========================================================================
  242. // Handler for when user clicks 'Save' in the settings window.
  243. //-------------------------------------------------------------------
  244. function settings_saved(new_settings) {
  245. init_ui();
  246. populate_item_info('radical');
  247. populate_item_info('kanji');
  248. }
  249.  
  250. //========================================================================
  251. // Populate level info from API.
  252. //-------------------------------------------------------------------
  253. function process_items(data) {
  254. gobj.items = wkof.ItemData.get_index(data, 'item_type');
  255.  
  256. populate_item_info('radical');
  257. populate_item_info('kanji');
  258. }
  259.  
  260. //========================================================================
  261. // Generate content for popover.
  262. //-------------------------------------------------------------------
  263. let srs_stages = ['Lesson','Apprentice 1','Apprentice 2','Apprentice 3','Apprentice 4','Guru 1','Guru 2','Master','Enlightened','Burned'];
  264. srs_stages[-1] = 'Locked';
  265. function generate_item_info() {
  266. // Populate the next review date.
  267. let elem = $(this);
  268. let item = elem.data('item');
  269. let html = [];
  270.  
  271. // Functions for filtering and sorting information.
  272. function accepted_first(a, b) {
  273. if (a.accepted_answer === b.accepted_answer) return 0;
  274. if (a.accepted_answer) return -1;
  275. return 1;
  276. }
  277. function primary(a) {return a.primary;}
  278. function to_meaning(a) {return a.meaning;}
  279. function to_reading(a) {return a.reading;}
  280.  
  281. // Meaning
  282. if (settings.show_meaning) {
  283. let meaning = item.data.meanings.filter(primary).sort(accepted_first).map(to_meaning).join(', ');
  284. html.push('<span class="meaning">'+meaning+'</span>');
  285. }
  286.  
  287. // Reading
  288. if (settings.show_reading && item.object === 'kanji') {
  289. let reading = item.data.readings.filter(primary).sort(accepted_first).map(to_reading).join(', ');
  290. html.push('<span class="reading" lang="ja">'+reading+'</span>');
  291. }
  292.  
  293. // SRS Stage
  294. if (settings.show_srs && item.assignments && item.assignments.srs_stage) {
  295. html.push('<span class="srs">SRS: '+srs_stages[item.assignments.srs_stage]+'</span>');
  296. }
  297.  
  298. // Pass Date and Next Review
  299. let next = [];
  300. let date;
  301. if (item.assignments) {
  302. if (item.assignments.srs_stage == 9) {
  303. if (settings.show_passed) {
  304. date = formatDate(new Date(item.assignments.burned_at), false /* is_next_date */);
  305. next.push('Burned: '+date);
  306. } else {
  307. next.push('Burned!');
  308. }
  309. } else if (item.assignments.available_at) {
  310. if (item.assignments.passed_at) {
  311. if (settings.show_passed) {
  312. if (item.assignments.passed_at) {
  313. date = formatDate(new Date(item.assignments.passed_at), false /* is_next_date */);
  314. } else {
  315. date = 'A long time ago...';
  316. }
  317. next.push('Passed: '+date);
  318. }
  319. }
  320. if (settings.show_next_review) {
  321. date = formatDate(new Date(item.assignments.available_at), true /* is_next_date */);
  322. next.push('Next: '+date);
  323. }
  324. } else if (item.assignments.unlocked_at) {
  325. next.push('Lesson: Available Now');
  326. } else {
  327. next.push('Locked!');
  328. }
  329. } else {
  330. next.push('Locked!');
  331. }
  332.  
  333. // Populate remaining data for popup window.
  334. if (next.length !== 0) {
  335. html.push('<span class="next">'+next.join('<br>')+'</span>');
  336. }
  337.  
  338. return html.join('<br>');
  339. }
  340.  
  341. //========================================================================
  342. // Determine whether the popover should be to the left or right of the element.
  343. //-------------------------------------------------------------------
  344. function place_item_info() {
  345. let elem = this.$element.eq(0);
  346. let parent = elem.parent();
  347. return ((elem.position().left + elem.width() - parent.position().left) <= (parent.width()/2) ? 'right' : 'left');
  348. }
  349.  
  350. //========================================================================
  351. // Determine whether the item is unlocked.
  352. //-------------------------------------------------------------------
  353. function is_unlocked(item) {
  354. return (item && item.assignments && item.assignments.unlocked_at ? true : false);
  355. }
  356.  
  357. //========================================================================
  358. // Determine whether the item is "Initiate" stage (i.e. unlocked but lesson not done).
  359. //-------------------------------------------------------------------
  360. function is_initiate(item) {
  361. return (item && item.assignments && item.assignments.unlocked_at ? true : false);
  362. }
  363.  
  364. //========================================================================
  365. // Determine whether the item has been previously Guru'd.
  366. //-------------------------------------------------------------------
  367. function is_passed(item) {
  368. return (item && item.assignments && item.assignments.passed_at ? true : false);
  369. }
  370.  
  371. //========================================================================
  372. // Populate level info from API.
  373. //-------------------------------------------------------------------
  374. function populate_item_info(itype) {
  375. let group,elems,items;
  376. let section_idx = Array.from(document.querySelectorAll('.level-progress-dashboard__content-title')).map((e)=>e.textContent.trim().toLowerCase().slice(0,3)).indexOf(itype.slice(0,3));
  377. if (section_idx < 0) return;
  378. group = $('.level-progress-dashboard__items').eq(section_idx);
  379. group.attr('data-type',itype);
  380. elems = group.find('.level-progress-dashboard__item');
  381. items = wkof.ItemData.get_index(gobj.items[itype], 'slug');
  382.  
  383. // Populate item data.
  384. elems.each(function(idx, elem){
  385. elem = $(elem);
  386. let a = elem.find('a');
  387. a.removeAttr('title');
  388. let slug;
  389. if (itype === 'radical') {
  390. slug = decodeURIComponent(a.attr('href').split('/').slice(-1)[0]);
  391. } else {
  392. slug = decodeURIComponent(a[0].innerText.trim());
  393. }
  394. let item = items[slug];
  395. elem.data('item', item);
  396.  
  397. elem.addClass('dpp-progress');
  398.  
  399. // Populate 'data-srs-lvl', which is a styling selector.
  400. let srs = (item.assignments && item.assignments.srs_stage ? item.assignments.srs_stage : (is_initiate(item) ? 0 : -1)); // -1 == locked
  401. elem.attr('data-srs-lvl', srs);
  402. });
  403.  
  404. // Sort items by srs level, then review date, then meaning.
  405. if (typeof elems.eq(0).data('original_position') !== 'number') {
  406. elems.each((idx, elem) => $(elem).data('original_position', idx));
  407. }
  408. let locked_last = (settings.locked_position === 'last');
  409. let srs_locked = (locked_last ? 10 : -1);
  410. elems.sort(function(a,b){
  411. let a_pos = $(a).data('original_position');
  412. let b_pos = $(b).data('original_position');
  413. if (itype === 'radical') {
  414. a = items[$(a).find('a').attr('href').split('/').slice(-1)[0]];
  415. b = items[$(b).find('a').attr('href').split('/').slice(-1)[0]];
  416. } else {
  417. a = items[a.innerText];
  418. b = items[b.innerText];
  419. }
  420. if (!locked_last) {
  421. let a_passed = is_passed(a);
  422. let b_passed = is_passed(b);
  423. if (!a_passed && b_passed) return -1;
  424. if (a_passed && !b_passed) return 1;
  425. }
  426. let a_srs = (a && a.assignments && a.assignments.srs_stage ? a.assignments.srs_stage : (is_initiate(a) ? 0 : srs_locked));
  427. let b_srs = (b && b.assignments && b.assignments.srs_stage ? b.assignments.srs_stage : (is_initiate(b) ? 0 : srs_locked));
  428. if (a_srs < b_srs) return -1;
  429. if (a_srs > b_srs) return 1;
  430. if (a_srs != 0) {
  431. let a_avail = (a && a.assignments && a.assignments.available_at ?
  432. new Date(a.assignments.available_at).getTime() : Number.MAX_SAFE_INTEGER);
  433. let b_avail = (b && b.assignments && b.assignments.available_at ?
  434. new Date(b.assignments.available_at).getTime() : Number.MAX_SAFE_INTEGER);
  435. if (a_avail < b_avail) return 1;
  436. if (a_avail > b_avail) return -1;
  437. } else {
  438. if (settings.lesson_order === 'wanikani') {
  439. return (a_pos < b_pos ? -1 : 1);
  440. }
  441. }
  442. if (a.data.slug < b.data.slug) return -1;
  443. if (a.data.slug > b.data.slug) return 1;
  444. return 0;
  445. });
  446. elems.detach().appendTo(group);
  447.  
  448. elems.removeClass('dpp-noshow pct90_left pct90 pct90_right');
  449. let srslvl;
  450. switch (settings.visible_items) {
  451. case 'non_passed':
  452. elems.each(function(idx, elem){
  453. elem = $(elem);
  454. let item = items[elem.text()];
  455. if (is_passed(item)) elem.addClass('dpp-noshow');
  456. });
  457. break;
  458. case 'passed':
  459. elems.each(function(idx, elem){
  460. elem = $(elem);
  461. let item = items[elem.text()];
  462. if (!is_passed(item)) elem.addClass('dpp-noshow');
  463. });
  464. break;
  465. }
  466.  
  467. if (settings.show_90percent && settings.visible_items !== 'passed' && itype === 'kanji') {
  468. // Add marker at 90%, indicating when level will be complete.
  469. let needed = Math.ceil(elems.length * 0.9);
  470. let locked = 0;
  471. let passed = 0;
  472. let passed_hidden = 0;
  473. let visible = 0;
  474. elems.each(function(idx, elem){
  475. elem = $(elem);
  476. let item = items[$(elem).text()];
  477. if (is_passed(item)) {
  478. passed++;
  479. if (elem.hasClass('dpp-noshow')) passed_hidden++;
  480. }
  481. if (!is_unlocked(item)) locked++;
  482. });
  483.  
  484. let visible_elems = elems.filter(':not(.dpp-noshow)');
  485. let visible_len = visible_elems.length;
  486.  
  487. let first = elems.length - needed;
  488. let last = first + (needed - 1) - passed_hidden;
  489. if (locked_last) {
  490. let shift = Math.min(first, locked);
  491. first -= shift;
  492. last -= shift;
  493. }
  494. if (first <= last) {
  495. visible_elems.eq(first).addClass('pct90_left');
  496. visible_elems.slice(first, last + 1).addClass('pct90');
  497. visible_elems.eq(last).addClass('pct90_right');
  498. }
  499. }
  500. }
  501.  
  502. //========================================================================
  503. // Print date in pretty format.
  504. //-------------------------------------------------------------------
  505. function formatDate(d, is_next_date){
  506. let s = '';
  507. let now = new Date();
  508. let YY = d.getFullYear(),
  509. MM = d.getMonth(),
  510. DD = d.getDate(),
  511. hh = d.getHours(),
  512. mm = d.getMinutes(),
  513. one_day = 24*60*60*1000;
  514.  
  515. if (is_next_date && d < now) return "Available Now";
  516. let same_day = ((YY == now.getFullYear()) && (MM == now.getMonth()) && (DD == now.getDate()) ? 1 : 0);
  517.  
  518. // If today: "Today 8:15pm"
  519. // otherwise: "Wed, Apr 15, 8:15pm"
  520. if (same_day) {
  521. s += 'Today ';
  522. } else {
  523. s += ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()]+', '+
  524. ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][MM]+' '+DD+', ';
  525. }
  526. if (settings.time_format === '24hour') {
  527. s += ('0'+hh).slice(-2)+':'+('0'+mm).slice(-2);
  528. } else {
  529. s += (((hh+11)%12)+1)+':'+('0'+mm).slice(-2)+['am','pm'][Math.floor(d.getHours()/12)];
  530. }
  531.  
  532. // Append "(X days)".
  533. if (is_next_date && !same_day) {
  534. let days = (Math.floor((d.getTime()-d.getTimezoneOffset()*60*1000)/one_day)-Math.floor((now.getTime()-d.getTimezoneOffset()*60*1000)/one_day));
  535. if (days) s += ' ('+days+' day'+(days>1?'s':'')+')';
  536. }
  537.  
  538. return s;
  539. }
  540.  
  541. })(window.dpp);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址