您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Make a temporary projections page for WKStats
// ==UserScript== // @name WKStats Projections Page // @version 1.4.1 // @description Make a temporary projections page for WKStats // @author UInt2048 // @match https://www.wkstats.com/* // @run-at document-end // @grant none // @namespace https://greasyfork.org/users/684166 // ==/UserScript== //jshint esversion:6 /*eslint max-len: ["error", { "code": 120 }]*/ (function() { "use strict"; Date.prototype.add = function(seconds) { return this.setTime(this.getTime() + (seconds*1000)) && this; }; Date.prototype.subtractDate = function(date) { return (this.getTime() - date.getTime()) / 1000; }; Date.prototype.format = function() { return new window.Intl.DateTimeFormat("default", { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'}).format(this); }; const P = { maxLevel: null, progressions: [], stats: null, now: null, addGlobalStyle: function addGlobalStyle() { const head = document.getElementsByTagName("head")[0], css = ` .main-content .chart table.coverage {margin-top:1em; position:relative;} .main-content .chart table.coverage {border-collapse:collapse; border-spacing:0; margin-left:auto; margin-right:auto;} .main-content .chart table.coverage tr {border-left:1px solid #000;} .main-content .chart table.coverage tr:first-child {border-top:1px solid #000;} .main-content .chart table.coverage tr:last-child {border-bottom:1px solid #000;} .main-content .chart table.coverage tr.header {background-color:#ffd; font-weight:bold;} .main-content .chart table.coverage tr.header:nth-child(2) {line-height:1em;} .main-content .chart table.coverage tr.header.bottom {border-bottom:1px solid #000;} .main-content .chart table.coverage tr:not(.header) + tr:not(.header):not(.current_level) {border-top:1px solid #ddd;} .main-content .chart table.coverage tr:not(.header):nth-child(even) {background-color:#efe;} .main-content .chart table.coverage td {padding:0 .5em;} .main-content .chart table.coverage td:first-child {border-right:1px solid #000;} .main-content .chart table.coverage td:last-child {border-right:1px solid #000;} .main-content .chart table.coverage tr.header td.header_div {border-bottom:1px solid #0001;} .main-content .chart table.coverage tr.count td {font-weight:normal; font-size:0.625em;} .main-content .chart table.coverage tr.current_level {border:2px solid #000;} .main-content .chart table.coverage tr.current_level:after {content:"\f061"; font-family:FontAwesome; position:absolute; display:inline-block; left:-1.25em;} `; if (head) { const style = document.createElement("style"); style.type = "text/css"; style.innerHTML = css.replace(/;/g, " !important;"); head.appendChild(style); } }, median: function median(arr) { const mid = Math.floor(arr.length / 2); return arr.length === 0 ? 0 : arr.length % 2 !== 0 ? arr[mid] : (arr[mid - 1] + arr[mid]) / 2; }, countComponent: function(componentLevel, itemLevel) { // For items in future levels, don't count passing time for components on preceding levels return !(itemLevel > P.progressions[P.progressions.length - 1].level && componentLevel < itemLevel); }, levelDuration: function(level) { return new Date(level.passed_at || level.abandoned_at).subtractDate(new Date(level.unlocked_at)); }, getLater: function(a, b) { return new Date(Math.max(a, b)); }, getFools: function(date, fools) { return fools ? new Date(date.getFullYear() + (date.getMonth() >= 3), 3, 1) : date; }, get: function(a, b) { return a && a[b]; }, getID: function(a, b) { return P.get(document.getElementById(a), b); }, getHyp: function(fastest, isCurrent) { const s = isCurrent ? "current" : fastest; return P.getID("speed" + (P.getID("hypothetical", "checked") ? "-" + s : ""), "value") * 3600 || 864000; }, formatInterval: function(seconds) { const days = seconds / 86400; const hours = (days % 1) * 24; const minutes = (hours % 1) * 60; const secs = (minutes % 1) * 60; return `${Math.floor(days)}d ${Math.floor(hours)}h ${Math.floor(minutes)}m ${Math.floor(secs)}s`; }, findLevel: function(levels, level) { return levels.slice().reverse().find(p => level == p.level); }, rangeFormat: function(arr) { return arr.map((n, i) => i < arr.length - 1 && arr[i + 1] - n === 1 ? `${i > 0 && n - arr[i - 1] === 1 ? "" : n}-` : `${n}, ` ).join("").replace(/-+/g, "-").slice(0, -2); }, project: function project() { const current = P.progressions[P.progressions.length - 1]; const levels = P.progressions.slice().concat(Array.from({length: P.maxLevel - current.level + 2}, (_, i) => ({level: current.level + 1 + i}))); const medianSpeed = P.median(P.progressions.slice(0, -1).map(P.levelDuration).sort((a, b) => a - b)); const showPast = P.getID("showPast", "checked"); const fools = P.getID("fools", "checked"); const hypothetical = P.getID("hypothetical", "checked"); const time = P.stats.map(d => d.length && d.sort((a, b) => a[0] - b[0])[Math.ceil(d.length * 0.9) - 1][0]); const expanded = P.getID("expand", "checked") && P.findLevel(levels, P.getID("expanded", "value")); const u = time.map((d, i) => { const unlocked = P.get(P.findLevel(levels, i), "unlocked_at"); return [(unlocked ? P.now.subtractDate(new Date(unlocked)) : 0) + d, i]; }); let output = `<input type="checkbox" id="expand" class="project" ${expanded ? "checked" : ""}> <label for="speed">Show Details for Level:</label> <input type="number" id="expanded" size="3" value="${P.get(expanded, "level") || current.level}"><br/> <input type="checkbox" id="showPast" class="project" ${showPast ? "checked" : ""}> <label for="showPast">Show Past Levels</label><br/> <input type="checkbox" id="fools" class="project" ${fools ? "checked" : ""}> <label for="fools">Dark Blockchain</label><br/> <input type="checkbox" id="hypothetical" class="project" ${hypothetical ? "checked" : ""}> <label for="hypothetical">Expand Hypothetical</label><br/> ${hypothetical ? Array.from(new Set(u.slice(current.level, -1).map(d => d[0]))).map((time, i) => { const s = i === 0 ? "current" : time; return `<label for="speed-${s}">Hypothetical Speed for fastest ${P.formatInterval(time)} (levels ${P.rangeFormat(u.filter((d, i) => time === d[0]).map(d => d[1]))}):</label> <input type="number" id="speed-${s}" size="4" value="${P.getHyp(time, i === 0) / 3600}">h<br/>`; }).reduce((a, b) => a + b) : `<label for="speed">Hypothetical Speed:</label> <input type="number" id="speed" size="4" value="${P.getHyp(time) / 3600}">h`} <button id="project" class="project">Project</button><br/> <table class="coverage"><tbody><tr class="header"> ${expanded ? "<td>Kanji</td><td colspan=3>Fastest</td>" : "<td>Level </td><td> Real/Predicted </td><td> Fastest </td><td> Hypothetical</td>"}</tr>`, unlocked = new Date(P.now), currentReached = false, info = "", real = null, fastest = null, given = null, p = []; for (const i of levels) { if (i === current) currentReached = true; if (!showPast && !currentReached) continue; const l = i.level, _fastest = new Date(fastest || P.now).add(time[l - 1]), _real = P.getLater(new Date(real || unlocked).add(l === P.maxLevel + 2 ? time[l - 1] : medianSpeed), _fastest), _given = P.getLater(new Date(given || unlocked).add(l === P.maxLevel + 2 ? time[l - 1] : P.getHyp(time[l - 1], l === current.level + 1)), _fastest); if (i.unlocked_at) { unlocked = new Date(i.unlocked_at); info = `<td> ${unlocked.format()} </td><td> - </td><td> - </td>`; } else if (l <= P.maxLevel) { p[l] = {fastest: P.getFools(fastest = _fastest, fools).format(), real: (real = _real).format(), given: (given = _given).format()}; info = `<td> ${p[l].real} </td><td> ${p[l].fastest} </td><td> ${p[l].given} </td>`; } else { p[l] = {fastest: P.getFools(_fastest, fools).format(), real: _real.format(), given: _given.format()}; info = `<td> ${p[l].real} </td><td> ${p[l].fastest} </td><td> ${p[l].given} </td>`; } if (!expanded) { output += `<tr ${i === current ? "class=\"current_level\"" : ""}><td> ${ l === P.maxLevel + 2 ? "全火" : String("0" + l).slice(-2)} </td> ${info} </tr>`; } else if (expanded === i) { for (const kan of P.stats[expanded.level]) { const date = (kan[0] < 0 ? "Passed on " : "") + (new Date(fastest || P.now)).add(kan[0]).format(); output += `<tr><td>${kan[1].data.characters}</td><td colspan=3>${date}</tr>`; } } } output += "</tbody></table>"; const element = document.getElementsByClassName("projections")[0]; if (!element.className.includes("chart")) element.className += " chart"; element.innerHTML = output; Array.from(document.getElementsByClassName("project")).forEach(x => x.addEventListener("click", project)); P.addGlobalStyle(); return JSON.stringify(Object.assign({}, p)); }, api: function api(userData, levels, systems, items) { if (P.progressions.length > 0) return P.project(); P.maxLevel = userData.subscription.max_level_granted; P.progressions = Object.values(levels).map(level => level.data); P.now = new Date(); const time = function(item, burn) { if (!P.get(item.assignments, burn ? "burned_at" : "passed_at")) { let interval = P.get(item.assignments, "available_at") ? Math.max(0, (new Date(item.assignments.available_at)).subtractDate(P.now)) : 0; const srs = systems[item.data.spaced_repetition_system_id].data; const target = P.get(srs, burn ? "burning_stage_position" : "passing_stage_position"); for (let i = (P.get(item.assignments, "srs_stage") || 0) + 1; i < target; i++) { interval += srs.stages[i].interval; } return interval; } return (new Date(P.get(item.assignments, burn ? "burned_at" : "passed_at"))).subtractDate(P.now); }; const unlock = function(item, itemLevel, burn) { return P.countComponent(item.data.level, itemLevel) ? (item.object === "radical" || item.object === "kana_vocabulary" ? 0 : item.data.component_subject_ids. map(id => Math.max(0, unlock(items.find(o => o.id === id), item.data.level))). reduce((a, b) => Math.max(a, b))) + time(item, burn) : 0; }; P.stats = Array.from(Array(P.maxLevel + 1), () => []); for (const item of items) { if (item.data.hidden_at || item.object !== "kanji") continue; P.stats[item.data.level].push([unlock(item, item.data.level, false), item]); } let burnStats = items.filter(item => !item.data.hidden_at).map(item => unlock(item, item.data.level, true)); P.stats.push([[burnStats.sort((a, b) => a - b)[burnStats.length - 1], burnStats]]); return P.project(); } }; window.wkof.include("ItemData, Apiv2"); window.wkof.ready("ItemData, Apiv2").then(() => { window.wkof.Apiv2.get_endpoint("user").then(userData => { window.wkof.Apiv2.get_endpoint("level_progressions").then(levels => { window.wkof.Apiv2.get_endpoint("spaced_repetition_systems").then(systems => { window.wkof.ItemData.get_items("subjects, assignments").then(items => { // Enable callback when we enter the progression page window.wkof.on("wkstats.projections.loaded", () => P.api(userData, levels, systems, items)); }); }); }); }); }); })();