您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows the earliest date and time you can level up if your reviews are correct. Adds an indication if you have items available for lesson/review that are needed to advance your current level.
当前为
- // ==UserScript==
- // @name Wanikani Level-Up Time Assistant
- // @namespace https://greasyfork.org/en/users/11878
- // @version 1.2.4
- // @description Shows the earliest date and time you can level up if your reviews are correct. Adds an indication if you have items available for lesson/review that are needed to advance your current level.
- // @author Inserio
- // @match https://www.wanikani.com/dashboard*
- // @match https://www.wanikani.com/
- // @grant none
- // @license MIT
- // ==/UserScript==
- /* global wkof */
- window.lu = {};
- (function(lu_obj) {
- //========================================================================
- // Initialization of the Wanikani Open Framework.
- //-------------------------------------------------------------------
- const script_name = 'Wanikani Level Up Time';
- let wkof_version_needed = '1.0.27';
- if (!window.wkof) {
- if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?'))
- window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
- return;
- }
- if (wkof.version.compare_to(wkof_version_needed) === 'older') {
- if (confirm(script_name+' requires Wanikani Open Framework version '+wkof_version_needed+'.\nDo you want to be forwarded to the update page?'))
- window.location.href = 'https://greasyfork.org/en/scripts/38582-wanikani-open-framework';
- return;
- }
- //========================================================================
- // Globals
- //-------------------------------------------------------------------
- var config = {
- log: {
- enabled: true,
- detailed: true
- },
- callback: null
- };
- var items_by_subject_id;
- var items_by_type;
- //========================================================================
- // Startup
- //-------------------------------------------------------------------
- wkof.include('ItemData');
- startup();
- wkof.ready('ItemData').then(fetch_items);
- function startup() {
- lu_obj.items_with_soonest_assignments = [];
- lu_obj.items_not_passed_with_assignments_available = [];
- install_css();
- init_ui();
- }
- /**
- * Install stylesheet.
- */
- function install_css() {
- const lu_css =
- '#lu_container { margin: auto auto 12px auto; color: #555; font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-weight: 300; text-shadow: 0 1px 0 #fff; font-size:13px; }'+
- '#lu_container i { padding-left:5px; padding-right:5px; font-size:14px; }';
- $('head').append('<style>'+lu_css+'</style>');
- }
- /**
- * Initialize the user interface.
- */
- function init_ui() {
- const html =
- "<div id='lu_container'>"+
- "<i id='lu_arrow_up' class='fa-solid fa-circle-up' title='"+get_text_for_icon_tooltip()+"'></i>"+
- "<strong>Earliest Level Up: </strong><span id='lu_level_up_date'></span>"+
- "</div>";
- $('.progress-and-forecast > .wk-panel--review-forecast > :first-child').before(html);
- }
- //========================================================================
- // Populate level info from API.
- //-------------------------------------------------------------------
- function fetch_items() {
- // Fetch only radicals and kanji for current level.
- // Include /subjects and /assignments endpoints
- wkof.ItemData.get_items({
- wk_items:{
- options:{
- assignments:true,
- review_statistics:true
- },
- filters:{
- level:'+0',
- item_type:'rad,kan',
- }
- }
- }).then(prepare_items);
- }
- function prepare_items(items) {
- lu_obj.load_time = new Date();
- items_by_type = wkof.ItemData.get_index(items, 'item_type');
- items_by_subject_id = wkof.ItemData.get_index(items, 'subject_id');
- // Add "earliest_study_date" and "earliest_guru_date" properties to items
- // Need to parse radicals first so that locked kanji get the proper dates assigned to them
- add_dates_to_items((items_by_type.radical?items_by_type.radical:[]).concat(items_by_type.kanji));
- lu_obj.items = items.sort(get_sort_method('+earliest_study_date','+earliest_guru_date','+data.subject_id'));
- // Cache these filters for quick lookups
- lu_obj.items_not_locked_and_not_passed = get_not_locked_but_not_passed_items(lu_obj.items);
- lu_obj.items_not_passed_with_assignments_available = get_not_passed_items_with_available_assignments(lu_obj.items);
- lu_obj.items_with_soonest_assignments = get_next_soonest_study_items(lu_obj.items_not_locked_and_not_passed);
- // Log the results to the console
- log_base_items_stats();
- // Prevent non-intentional logging
- config.log.enabled = false;
- process_items();
- setup_next_reviews_callback();
- }
- function process_items() {
- lu_obj.level_up_date = get_level_up_date();
- update_ui();
- let lu_level_up_date = document.getElementById('lu_level_up_date');
- let lu_arrow_up = document.getElementById('lu_arrow_up');
- if (lu_arrow_up) {
- lu_arrow_up.onmouseover = function(){update_ui('lu_arrow_up');};
- }
- if (lu_level_up_date) {
- lu_level_up_date.onmouseover = function(){update_ui('lu_level_up_date');};
- lu_level_up_date.onclick = function(){config.log.enabled = true; fetch_items();};
- }
- }
- function update_date_title() {
- let lu_level_up_date = document.getElementById('lu_level_up_date');
- if (!lu_level_up_date) return;
- let dateOutput = format_date_to_standard_output(lu_obj.level_up_date, false);
- let wait_time = format_two_dates_diff_to_minimal_output(lu_obj.level_up_date,lu_obj.load_time, true);
- lu_level_up_date.innerHTML = dateOutput;
- lu_level_up_date.title = (wait_time==='Now'?'Available now':wait_time)+'\nClick to update data (and repeat logging to console)';
- }
- function update_arrow_title() {
- let lu_arrow_up = document.getElementById('lu_arrow_up');
- if (!lu_arrow_up) return;
- let title = '';
- if (lu_obj.items_with_soonest_assignments.length>0) {
- let review_time = format_date_to_standard_output(lu_obj.items_with_soonest_assignments[0].earliest_study_date,true, true);
- let next_items = lu_obj.items_with_soonest_assignments;
- if (review_time === 'Now') {
- title = `${lu_obj.items_with_soonest_assignments.length} item${lu_obj.items_with_soonest_assignments.length===1?' needed to pass this level is':'s needed to pass this level are'} currently available to study\nClick here to proceed to a new session`;
- next_items = unique_from(get_next_soonest_study_items(lu_obj.items_not_locked_and_not_passed,lu_obj.items_with_soonest_assignments));
- if (next_items.length>0) {
- title += '\n\n';
- review_time = format_date_to_standard_output(next_items[0].earliest_study_date,true, true);
- }
- }
- if (next_items.length>0) {
- title += `The next ${next_items.length} item${next_items.length>1?'s':''} of the ones needed to pass this level will arrive at:\n${(review_time)}`;
- }
- } else {
- title = get_text_for_icon_tooltip();
- }
- lu_arrow_up.title = title;
- if (lu_obj.items_not_passed_with_assignments_available.length>0) {
- let destination = lu_obj.items_not_passed_with_assignments_available.find(a=>!a.assignments.available_at) ? 'lesson' : 'review';
- lu_arrow_up.onclick = function(){window.location='https://www.wanikani.com/subjects/'+destination;};
- lu_arrow_up.style.setProperty('color', '#00ff00');
- lu_arrow_up.classList.add("fa-beat");
- } else {
- lu_arrow_up.onclick = null;
- lu_arrow_up.style.removeProperty('color');
- lu_arrow_up.classList.remove("fa-beat");
- }
- }
- function update_ui(element_id) {
- lu_obj.load_time = new Date();
- if (!element_id || element_id === 'lu_level_up_date')
- update_date_title();
- if (!element_id || element_id === 'lu_arrow_up')
- update_arrow_title();
- }
- function setup_next_reviews_callback() {
- if (config.callback) {
- clearTimeout(config.callback);
- config.callback = null;
- }
- if (lu_obj.items_with_soonest_assignments.length<=0) return;
- let time_diff = lu_obj.items_with_soonest_assignments[0].earliest_study_date.getTime() - (new Date()).getTime();
- if (time_diff <= 0) return;
- setTimeout(function() {
- config.callback = null;
- fetch_items();
- }, time_diff);
- }
- function log_base_items_stats() {
- if (!config.log.enabled) return;
- const max_date_len = 48;
- let items_not_passed_by_type = wkof.ItemData.get_index(get_not_passed_items(lu_obj.items), 'item_type');
- let items_locked_by_type = wkof.ItemData.get_index(get_locked_items(lu_obj.items), 'item_type');
- for (let itype of Object.keys(items_not_passed_by_type).sort((a,b)=>a.localeCompare(b)*-1)) {
- let not_passed_items = items_not_passed_by_type[itype];
- if (!not_passed_items || not_passed_items.length<=0) continue;
- let str = [`${not_passed_items.length} remaining ${itype}${itype==='radical'&¬_passed_items.length>1?'s':''} to guru`+
- (items_locked_by_type[itype]&&items_locked_by_type[itype].length>0?' ('+items_locked_by_type[itype].length+' of which are still locked)':'')];
- if (config.log.detailed) {
- for (let i=0; i<not_passed_items.length; i++) {
- let itm = not_passed_items[i];
- let next_study_time = format_date_to_standard_output(itm.earliest_study_date,true);
- let earliest_guru_time = format_date_to_standard_output(itm.earliest_guru_date,true);
- str.push((!itm.assignments||!itm.assignments.unlocked_at?'🔒 ':'')+(itm.data.characters?itm.data.characters:itm.data.slug)+'\t'+
- '| Stage: '+(itm.assignments?itm.assignments.srs_stage:0)+'/5\t'+
- '| Next Study: '+next_study_time+'\t'.repeat(Math.ceil((max_date_len-next_study_time.length)/8))+
- '| Earliest Guru: '+earliest_guru_time+'\t'.repeat(Math.ceil((max_date_len-earliest_guru_time.length)/8))+
- '| '+itm.data.document_url);
- }
- }
- console.log(str.join('\n'));
- }
- }
- //========================================================================
- // Formatting
- //-------------------------------------------------------------------
- function get_text_for_icon_tooltip() {
- return `${lu_obj.items_not_passed_with_assignments_available.length} item${lu_obj.items_not_passed_with_assignments_available.length===1?'':'s'} needed to level ${(lu_obj.items_not_passed_with_assignments_available.length===1?'is':'are')} currently available to study`;
- }
- function format_two_dates_diff_to_minimal_output(date, date2, include_seconds) {
- let diff = Math.max(0, Math.trunc(date.getTime()/1000)-Math.trunc(date2.getTime()/1000));
- let dd = Math.floor(diff / 86400);
- diff -= dd*86400;
- let hh = Math.floor(diff / 3600);
- diff -= hh*3600;
- let mm = Math.floor(diff / 60);
- diff -= mm*60;
- let ss = diff;
- if (dd > 0) {
- return dd+' day'+(dd===1?'':'s')+', '+hh+' hour'+(hh===1?'':'s')+', '+mm+' min'+(mm===1?'':'s')+(include_seconds?', '+ss+' sec'+(ss===1?'':'s'):'');
- } else if (hh > 0) {
- return hh+' hour'+(hh===1?'':'s')+', '+mm+' min'+(mm===1?'':'s')+(include_seconds?', '+ss+' sec'+(ss===1?'':'s'):'');
- } else if (mm > 0 || ss > 0) {
- if (!include_seconds && ss > 30) mm++;
- return mm+' min'+(mm===1?'':'s')+(include_seconds?', '+ss+' sec'+(ss===1?'':'s'):'');
- } else {
- return 'Now';
- }
- }
- function format_date_to_standard_output(date, include_differential, include_seconds) {
- if (!(date instanceof Date)) date = new Date(date);
- if (date.getTime() <= lu_obj.load_time.getTime()) return "Now";
- let str = date.toLocaleString([],{weekday:"short",month:"short",day:"numeric",hour12:false,hour:"numeric",minute:"numeric"});
- if (!include_differential) return str;
- return str +' ('+format_two_dates_diff_to_minimal_output(date,lu_obj.load_time, include_seconds)+')';
- }
- //========================================================================
- // Transformers and Helpers
- //-------------------------------------------------------------------
- /**
- * Returns a non-destructive Array of elements that are not found in
- * any of the parameter arrays.
- * Assumes all items have a property named "id"
- *
- * @param {...Array} var_args Arrays to compare.
- */
- function unique_from(arr1, ...args) {
- if (!args.length) return [];
- let out = [];
- let map = new Map();
- for (let n=0; n < args.length; n++) {
- let a2 = args[n];
- if (!(a2 instanceof Array))
- throw new TypeError( 'argument ['+n+'] must be an Array' );
- // Add existing id from the array to the map
- for (let i=0; i<a2.length; i++)
- map.set(a2[i].id, true);
- }
- // Add to the new array all items that aren't included in the map (map lookUp is O(1) complexity)
- for(let i=0; i<arr1.length; i++)
- if (!map.get(arr1[i].id))
- out.push(arr1[i]);
- return out;
- }
- /**
- * Get an Array sort function with multiple subarray fields.
- * Prefix letter allows specifying whether to sort ascending "+" or descending "-" (default: ascending)
- * Splits and recurses properties properly on periods
- * e.g.
- * let arr = [{obj:{prop1:'1'}, prop2: 3},{obj:{prop1:'3'}, prop2: 4},{obj:{prop1:'3'}, prop2: 2},{obj:{prop1:'4'}, prop2: 2}];
- * arr.sort(get_sort_method('+prop2','-obj.prop1'));
- * // [{obj:{prop1:'4'}, prop2: 2},{obj:{prop1:'3'}, prop2: 2},{obj:{prop1:'1'}, prop2: 3},{obj:{prop1:'3'}, prop2: 4}]
- */
- function get_sort_method(){
- let argsArr = Array.prototype.slice.call(arguments);
- return function(a, b){
- for(let x in argsArr){
- let strStart = 1;
- let op = argsArr[x].substring(0,1);
- if (op !== "-" && op !== "+") {op = "+";strStart = 0;}
- let prop = argsArr[x].substring(strStart);
- prop = prop.split('.');
- let len = prop.length;
- let i = 0;
- let ax = a;
- let bx = b;
- let cx;
- while(i<len) {ax = ax[prop[i]]; bx = bx[prop[i]]; i++;}
- ax = typeof ax == "string" ? ax.toLowerCase() : ax / 1;
- bx = typeof bx == "string" ? bx.toLowerCase() : bx / 1;
- if(op === "-"){cx = ax; ax = bx; bx = cx;}
- if(ax !== bx){return ax < bx ? -1 : 1;}
- }
- };
- }
- function add_dates_to_items(items) {
- for (let i=0; i<items.length; i++) {
- let itm = items[i];
- itm.earliest_study_date = itm.assignments && itm.assignments.unlocked_at ? new Date(itm.assignments.started_at ? itm.assignments.available_at : itm.assignments.unlocked_at) : get_earliest_unlock_date(itm);
- itm.earliest_guru_date = get_item_guru_date(itm);
- }
- }
- function get_not_passed_items(items) {
- let output_items = [];
- for (let i=0; i<items.length; i++) {
- let itm = items[i];
- if (!itm.assignments || !itm.assignments.passed_at)
- output_items.push(itm);
- }
- return output_items;
- }
- function get_not_locked_but_not_passed_items(items) {
- let output_items = [];
- for (let i=0; i<items.length; i++) {
- let itm = items[i];
- if (itm.assignments && itm.assignments.unlocked_at && !itm.assignments.passed_at)
- output_items.push(itm);
- }
- return output_items;
- }
- function get_not_passed_items_with_available_assignments(items) {
- let output_items = [];
- for (let i=0; i<items.length; i++) {
- let itm = items[i];
- if (itm.assignments && !itm.assignments.passed_at && itm.earliest_study_date.getTime()<=lu_obj.load_time.getTime())
- output_items.push(itm);
- }
- return output_items;
- }
- function get_locked_items(items) {
- let output_items = [];
- for (let i=0; i<items.length; i++) {
- let itm = items[i];
- if (!itm.assignments || !itm.assignments.unlocked_at)
- output_items.push(itm);
- }
- return output_items;
- }
- function get_next_soonest_study_items(items) {
- return items.reduce((acc,itm)=>{
- let min_date = acc.length>0 ? acc[0].earliest_study_date : itm.earliest_study_date;
- if (itm.earliest_study_date.getTime() > lu_obj.load_time.getTime() && itm.earliest_study_date.getTime() < min_date.getTime()) {
- min_date = itm.earliest_study_date;
- acc.length = 0;
- }
- if (itm.earliest_study_date.getTime() <= lu_obj.load_time.getTime() || itm.earliest_study_date.getTime() === min_date.getTime())
- acc.push(itm);
- itm.earliest_guru_date = get_item_guru_date(itm);
- return acc;
- },[]);
- }
- /**
- * Gets the earliest date that the provided item can be unlocked if all components are gurued as soon as possible
- */
- function get_earliest_unlock_date(item) {
- let min_date;
- for (let rad_idx = 0; rad_idx < item.data.component_subject_ids.length; rad_idx++) {
- let rad = items_by_subject_id[item.data.component_subject_ids[rad_idx]];
- if (!rad) continue;
- if (!min_date || rad.earliest_guru_date.getTime() > min_date.getTime())
- min_date = new Date(rad.earliest_guru_date);
- }
- return min_date;
- }
- /**
- * Calculate item guru date
- */
- function get_item_guru_date(item){
- let hours_to_guru = 0;
- if (!item || !item.earliest_study_date && (!item.assignments || !item.assignments.unlocked_at))
- return new Date(0); // This is mostly for debugging. If you see the 12/31/1969 date anywhere, this is where it went wrong.
- if (item.assignments && item.assignments.passed_at)
- return new Date(item.assignments.passed_at);
- switch (item.assignments ? item.assignments.srs_stage : 0) {
- case 0: hours_to_guru += 4+8+23+47; break;
- case 1: hours_to_guru += 8+23+47; break;
- case 2: hours_to_guru += 23+47; break;
- case 3: hours_to_guru += 47; break;
- }
- // Add the hours to the available date, or the unlock date if the item is locked, or current date something went wrong
- let earliest_guru_date = item.earliest_study_date ? new Date(item.earliest_study_date) : (item.assignments ? new Date(item.assignments.available_at ? item.assignments.available_at : item.assignments.unlocked_at) : new Date(lu_obj.load_time));
- if (earliest_guru_date.getTime() < lu_obj.load_time.getTime())
- earliest_guru_date = new Date(lu_obj.load_time);
- earliest_guru_date.setHours(earliest_guru_date.getHours() + hours_to_guru);
- return earliest_guru_date;
- }
- /**
- * Get earliest possible level up date
- * Calculated by sorting the kanji by earliest possible guru date and taking the time from the 90% to last kanji
- */
- function get_level_up_date() {
- let kanji_items = items_by_type.kanji.sort((a,b)=>a.earliest_guru_date.getTime()-b.earliest_guru_date.getTime());
- return new Date(kanji_items[Math.ceil(kanji_items.length * 0.9)-1].earliest_guru_date);
- }
- })(window.lu);