您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fix weird issues with the levelups on wkstats that are assumed to be caused by new additions of kanji to levels
// ==UserScript== // @name WKStats Levelup Fix // @namespace https://greasyfork.org/en/users/11878 // @version 2.0.2 // @description Fix weird issues with the levelups on wkstats that are assumed to be caused by new additions of kanji to levels // @author Inserio // @match https://www.wkstats.com/progress/dashboard // @match https://www.wkstats.com/progress/level-up // @match https://www.wkstats.com/progress/projections // @match https://www.wkstats.com/ // @icon https://www.google.com/s2/favicons?sz=64&domain=wkstats.com // @license MIT; http://opensource.org/licenses/MIT // @run-at document-start // @grant none // ==/UserScript== /*global wkof, wkdata, wkstats, calc_levelups, log, yyyymmdd, duration, wklogs*/ (function() { 'use strict'; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } function init() { const calc_stats = wkof.support_files['calc_stats.js']; if (calc_stats === undefined || calc_stats === null || !calc_stats.includes('1.0.7')) // only run on this version return; wkof.wait_state(/* state_var */ "wkof.wkstats.levelups",/* value */ "ready",/* callback */ overwriteFunction,/* persistent */ false); } function overwriteFunction() { console.log('Overriding the calc_levelups() function definition'); // eslint-disable-next-line no-global-assign calc_levelups = function() { wklogs['levelups'].length = 0; // clear existing logs (comment out if you want to compare results with the default version, which gets run first) log('levelups', 'Overriding previous data of calc_levelups() with modified version'); let level_times = wkstats.level_times = []; // For each level, initialize a valid range of possible level times (initial = any time) for (let level = 1; level <= wkof.user.subscription.max_level_granted; level++) { level_times[level] = { min: new Date(0), max: wkdata.load_time, dates: [], source: 'unknown', }; } // Using level resets, throw out old level start times (by marking the min start time) for (let reset_idx = 0; reset_idx < wkdata.resets.length; reset_idx++) { let reset = wkdata.resets[reset_idx]; let reset_time = new Date(reset.confirmed_at); for (let level = reset.target_level; level <= wkof.user.level; level++) { let level_time = level_times[level]; // Ignore resets that happened before this level-up. if (reset_time < level_time.min) continue; // Update the min start time. level_time.min = reset_time; delete level_times[level].reset_time; } level_times[reset.target_level].reset_time = reset_time; } // Using the newest levelup record for each level, set known start times. let oldest_levelup = {index: -1, time: new Date('2999-01-01')}; for (let levelup_idx = 0; levelup_idx < wkdata.levelups.length; levelup_idx++) { let levelup = wkdata.levelups[levelup_idx]; let level = levelup.level; if (level > wkof.user.level) continue; let unlocked_time = new Date(levelup.unlocked_at); let level_time = level_times[level]; // Check if this is the oldest recorded level-up, which may be invalid. if (unlocked_time < oldest_levelup.time) { oldest_levelup = {index: levelup_idx, time: unlocked_time, level: level}; } // Ignore levelups that were invalidated by a reset. if (unlocked_time < level_time.min) continue; // Update the level start time. level_time.min = unlocked_time; level_time.source = 'APIv2 level_progressions'; if (!levelup.abandoned_at && levelup.passed_at) { level_time.max = new Date(levelup.passed_at); } else if (level === wkof.user.level) { level_time.max = new Date(); } } let items = wkdata.items; let level_progressions = wkdata.levelups; let first_recorded_date = level_progressions[Math.min(...Object.keys(level_progressions))].unlocked_at; // Find indefinite level ups by looking at lesson history // Sort lessons by level then unlocked date items.forEach((item) => { if ( (item.object !== 'kanji' && item.object !== 'radical') || !item.assignments || !item.assignments.unlocked_at || item.assignments.unlocked_at >= first_recorded_date ) return; let date = new Date(item.assignments.unlocked_at); if (!level_times[item.data.level]) { level_times[item.data.level] = {}; } if (!level_times[item.data.level].dates[date.toDateString()]) { level_times[item.data.level].dates[date.toDateString()] = [date]; } else { level_times[item.data.level].dates[date.toDateString()].push(date); } }); // Discard dates with less than 10 unlocked // then discard levels with no dates // then keep earliest date for each level for (let [level, {min, max, dates, source}] of Object.entries(level_times)) { for (let [date, data] of Object.entries(dates)) { if (data.length < 10) delete dates[date]; } if (Object.keys(level_times[level].dates).length === 0) { delete level_times[level].dates; continue; } //level_times[level].min = Object.values(dates).reduce((low, curr) => (low < curr ? low : curr), Date.now()).sort((a, b) => (a.getTime() - b.getTime()))[0]; level_times[level].min = Object.values(dates).reduce((acc,item)=>{let smallest=item.reduce((a,b)=>a<b?a:b);return acc<smallest ? acc : smallest;}, new Date()); } // Map to array of [[level0, date0], [level1, date1], ...] Format //levels = Object.entries(levels).map(([level, date]) => [Number(level), date]); // Add definite level ups from API Object.values(level_progressions).forEach(lev => { if (level_times[lev.level].source === 'APIv2 level_progressions') return; level_times[lev.level] = { min: new Date(lev.unlocked_at), max: (lev.passed_at ? new Date(lev.passed_at) : wkdata.load_time), source: 'APIv2 level_progressions' };}); for (let level = 1; level <= wkof.user.level; level++) { let level_data = level_times[level]; if (level_data.source === 'APIv2 level_progressions') continue; if (level < level_times.length - 1) { let next_level_data = level_times[level+1]; if (level_data.max.getTime() === wkdata.load_time.getTime()) level_data.max = next_level_data.min; } } // Calculate durations let durations = wkstats.level_durations = []; for (let level = 1; level <= wkof.user.level; level++) { let level_time = level_times[level]; durations[level] = (level_time.max - level_time.min) / 86400000; } log('levelups','--[ Level-ups ]----------------'); let level_durations = wkstats.level_durations; // Log the current level statuses. log('levelups','Started: '+yyyymmdd(wkof.user.started_at)); if (wkof.user.restarted_at) { log('levelups','Restarted: '+yyyymmdd(wkof.user.restarted_at)); } for (let level = 1; level <= wkof.user.level; level++) { let level_time = level_times[level]; let level_duration = level_durations[level]; if (level_time.reset_time) { log('levelups','Reset'); } // Flag any unusual level durations. if (level < wkof.user.level && (level_duration < 3.0 || level_duration > 2000)) log('levelups','###################'); log('levelups','Level '+level+': ('+yyyymmdd(level_time.min)+' - '+yyyymmdd(level_time.max)+') - '+ duration(level_duration)+' (source: '+level_time.source+')'); } wkof.set_state('wkof.wkstats.levelups', 'ready'); }; // Immediately run in order to overwrite all of the configurations from the default run calc_levelups(); } })();