您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Calculates what day and time you can level up if your reviews are correct. Based off the version by Reeko182.
当前为
// ==UserScript== // @name Wanikani Level Up Time (Evolved) // @namespace https://greasyfork.org/en/users/11878 // @version 1.2.0 // @description Calculates what day and time you can level up if your reviews are correct. Based off the version by Reeko182. // @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-arrow-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'].concat(items_by_type['kanji'])); lu_obj.items = items.sort(get_sort_method('+earliest_study_date','+earliest_guru_date','+data.subject_id')); //lu_obj.items_accessible = get_accessible_items(lu_obj.items); //lu_obj.items_not_passed = get_not_passed_items(lu_obj.items); //lu_obj.items_inaccessible = get_inaccessible_items(lu_obj.items); //lu_obj.items_unlocked = get_unlocked_items(lu_obj.items); //lu_obj.items_locked = get_locked_items(lu_obj.items); // Finished non-dependant items 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_base_items_stats(); config.log.enabled = false; process_items(); setup_next_reviews_callback(); } // TODO: port this over to newer logic 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; //lu_obj.load_time = new Date(); 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');//lu_obj.items_not_passed, 'item_type'); let items_locked_by_type = wkof.ItemData.get_index(get_locked_items(lu_obj.items), 'item_type');//lu_obj.items_locked, '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. */ //Object.defineProperty(Array.prototype, 'unique_from', {value(){ 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_accessible_items(items) { let output_items = []; for (let i=0; i<items.length; i++) { let itm = items[i]; if (itm.assignments) output_items.push(itm); } return output_items; } function get_inaccessible_items(items) { let output_items = []; for (let i=0; i<items.length; i++) { let itm = items[i]; if (!itm.assignments) output_items.push(itm); } return output_items; } */ 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_unlocked_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; } /** * Get item guru date */ function get_item_guru_date(item){ //Calculate days to guru let hours_to_guru = 0; if (!item) hours_to_guru = 4+8+23+47; else if (!item.earliest_study_date && (!item.assignments || !item.assignments.unlocked_at)) { return new Date(0); } else { 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 or current date if item is locked 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; } 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);