您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
This is the WaniKani Customizer Chrome Extension Timeline ported to UserScript
当前为
// ==UserScript== // @name WaniKani Timeline // @namespace https://www.wanikani.com // @description This is the WaniKani Customizer Chrome Extension Timeline ported to UserScript // @version 0.1.2 // @include https://www.wanikani.com/ // @include https://www.wanikani.com/dashboard // @include https://www.wanikani.com/account // @run-at document-end // @grant none // ==/UserScript== /*jslint browser: true, plusplus: true*/ /*global $, console, alert, confirm */ /* Almost all of this code is by Kyle Coburn aka kiko on WakiKani. It has been reformatted slightly and some minor changes made. rev 0.1.1: added a few more options for classic style: enable only fuzzy_time_mode_past, fuzzy_time_mode_near, and optionally twelve_hour_mode rev 0.1.2: change to fix server timeouts on higher level vocabulary change to prevent always reloading data when no current reviews increased max display time to 7-days changed data storage format. now stores number and type of current and burning items. (for more display options later) added a last updated time (works with 'WaniKani Real Times') added a basic loading and error indicator added 'wkt_username_' prefix to localStorage keys (for multiple WK account support) added a reload button added invalid API key detection. forget bad key. get new on refresh. */ (function () { 'use strict'; var level, localStoragePrefix, tRes, ajaxCompletedCount, api_calls, api_colors, curr_date, start_time, gHours, graphH, graphH_canvas, styleCss, xOff, vOff, max_hours, times, pastReviews, firstReview, tFrac, page_width, options = {}; /* ### CONFIG OPTIONS ### */ // options.twelve_hour_mode = true; // enable 12-hour AM/PM mode options.fuzzy_time_mode_past = true; // enable '-x mins' mode for items now available // options.fuzzy_time_mode_near = true; // enable 'x mins' mode for upcoming items: now < time < now+90min options.fuzzy_time_paren = true; // append (x mins) to time for items: time < now+90min options.show_weekday = true; // show weekday prefix options.enable_arrows = true; // enable indicator arrows /* ### END CONFIG ### */ api_colors = ['#0096e7', '#ee00a1', '#9800e8']; curr_date = new Date(); start_time = curr_date.getTime() / 1000; gHours = 18; graphH = 88; graphH_canvas = graphH + 15; xOff = 18; vOff = 16; max_hours = 24 * 7; function strNumSeq(min, max) { var i, str = ''; for (i = min; i <= max; i++) { if (str) { str += ','; } str += i; } return str; } function addSplitVocab(level) { var segCnt, segLen, min, max, vocabRequestLevelSplitSize = 20; // maximum number of levels per vocab API request segCnt = Math.ceil(level / vocabRequestLevelSplitSize); segLen = Math.ceil(level / segCnt); for (min = 1; min <= level; min += segLen) { max = min + segLen - 1; if (max > level) { max = level; } api_calls.push('vocabulary/' + strNumSeq(min, max)); } } function getDashboardLevel() { var match, levelStr = $('section.progression h3').html(); if (levelStr) { match = levelStr.match(/Level (\d+) /); if (match && match.length === 2) { return parseInt(match[1], 10); } } return null; } api_calls = ['radicals', 'kanji']; level = getDashboardLevel(); if (level && 0 < level && level < 100) { // allow for level expansion addSplitVocab(level); } else { // if unknown level fail to no-split api_calls.push('vocabulary'); } function getPageUser() { var match, profileUrl = $('ul.nav a:contains("Profile")').prop('href'); if (profileUrl) { match = profileUrl.match('[^/]*$'); if (match && match.length === 1) { return match[0]; } } return ''; // blank if error } localStoragePrefix = 'wkt_' + getPageUser() + '_'; // Helpers function pluralize(noun, amount) { return amount + ' ' + (amount !== 1 ? noun + 's' : noun); } function fuzzyMins(minutes) { var seconds; if (minutes < 1 && minutes > 0) { seconds = Math.round(minutes * 60); return pluralize('sec', seconds); } minutes = Math.round(minutes); return pluralize('min', minutes); } // Draw function drawBarRect(ctx, xo, yo, bw, bh, color) { ctx.fillStyle = color; ctx.fillRect(xo, yo, bw, bh); } function drawBar(ctx, time, height, hOff, color, tFrac, outlined) { var bx = xOff + time * tFrac, by = graphH - height - hOff; ctx.fillStyle = color; ctx.fillRect(bx, by, tFrac, height); if (outlined) { ctx.strokeStyle = (outlined === -1 ? '#ffffff' : '#000000'); //ctx.strokeRect(bx, by, tFrac, hOff === 0 ? graphH : height); ctx.beginPath(); ctx.moveTo(bx, by + height); ctx.lineTo(bx, by); ctx.lineTo(bx + tFrac, by); ctx.lineTo(bx + tFrac, by + height); if (hOff !== 0) { // don't draw stroke on top of axis ctx.lineTo(bx, by + height); } ctx.stroke(); } } function drawArrow(ctx, currentType, xOff, time, tFrac) { if (currentType === -1) { ctx.fillStyle = '#FF0000'; } else if (currentType === -2) { ctx.fillStyle = '#A0A0A0'; } else { return; } var bx = xOff + (time - 1) * tFrac, topY = 3 + graphH, halfWidthX = tFrac / 2, cenX = bx + halfWidthX; if (halfWidthX > 9) { // limit arrow width halfWidthX = 9; } ctx.beginPath(); ctx.moveTo(cenX, topY); ctx.lineTo(cenX - halfWidthX, topY + 10); ctx.lineTo(cenX + halfWidthX, topY + 10); ctx.fill(); //ctx.lineTo(cenX, topY); //ctx.strokeStyle = "#000000"; //ctx.stroke(); } function drawCanvas(clear) { var totalCount, maxCount, graphTimeScale, ti, time, counts, total, ctx, gTip, pidx, canvasJQ, currentType, hrsFrac, gOff, height, count, i, width, hOff, xP, canvas = document.getElementById('c-timeline'), fuzzyExtra, weekdayText, weekday = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; if (canvas.getContext) { totalCount = 0; maxCount = 3; graphTimeScale = 60 * 60 * (gHours - 0.1); if (gHours === 0) { if (pastReviews) { for (ti = 0; ti < 3; ++ti) { totalCount += pastReviews[ti][0]; } maxCount = totalCount; } } else { for (time = 0; time < times.length; time++) { if (time * 60 * tRes < graphTimeScale) { counts = times[time]; if (counts) { total = 0; for (ti = 0; ti < 3; ++ti) { total += counts[ti][0]; } if (total > maxCount) { maxCount = total; } totalCount += total; } } } } if (totalCount === 0) { maxCount = 0; } $('#g-timereviews').text(totalCount); tFrac = tRes * (page_width - xOff) / 60 / gHours; ctx = canvas.getContext("2d"); if (clear) { ctx.clearRect(0, 0, page_width, graphH_canvas); page_width = $('.span12 header').width(); } else { gTip = $('#graph-tip'); canvasJQ = $('#c-timeline'); canvas.addEventListener('mousemove', function (event) { var reviewCount, currentCount, burnCount, idx, showTime, minDiff, tDisplay, tDate, hours, mins, suffix, tText; if (gHours === 0) { return; } //~ idx = Math.floor((event.offsetX - xOff) / tFrac) + 1; idx = Math.floor(((event.pageX - canvasJQ.offset().left) - xOff) / tFrac) + 1; if (idx !== pidx) { counts = times[idx]; if (counts) { gTip.show(); reviewCount = counts[0][0] + counts[1][0] + counts[2][0]; currentCount = counts[0][1] + counts[1][1] + counts[2][1]; burnCount = counts[0][2] + counts[1][2] + counts[2][2]; showTime = counts[3] * 1000; minDiff = (showTime - new Date().getTime()) / 1000 / 60; if (options.fuzzy_time_mode_past && minDiff < 0) { tDisplay = fuzzyMins(minDiff); } else if (options.fuzzy_time_mode_near && 0 <= minDiff && minDiff < 90) { tDisplay = fuzzyMins(minDiff); } else { tDate = new Date(showTime); hours = tDate.getHours(); mins = tDate.getMinutes(); suffix = ''; if (options.twelve_hour_mode) { suffix = ' ' + (hours < 12 ? 'am' : 'pm'); hours %= 12; if (hours === 0) { hours = 12; } } if (hours < 10) { hours = '0' + hours; } if (mins < 10) { mins = '0' + mins; } weekdayText = ''; if (options.show_weekday) { weekdayText = weekday[tDate.getDay()] + ' '; } fuzzyExtra = ''; if (options.fuzzy_time_paren && minDiff < 90) { fuzzyExtra = ' (' + fuzzyMins(minDiff) + ')'; } tDisplay = weekdayText + hours + ':' + mins + suffix + fuzzyExtra; } tText = tDisplay + '<br />' + pluralize('review', reviewCount); if (currentCount) { tText += '<br /><em>' + currentCount + ' current level</em>'; } if (burnCount) { tText += '<br /><em>' + burnCount + ' burning</em>'; } gTip.html(tText); gTip.css({ left: canvasJQ.position().left + idx * tFrac + xOff, top: event.pageY - gTip.height() - 6 }); } else { gTip.hide(); } pidx = idx; } else { gTip.css('top', event.pageY - gTip.height() - 6); } }, false); canvasJQ.mouseleave(function () { gTip.hide(); pidx = null; }); } canvas.width = page_width; hrsFrac = gHours / 3; ctx.lineWidth = tFrac / 20; ctx.strokeStyle = "#ffffff"; ctx.textBaseline = 'top'; ctx.textAlign = 'right'; ctx.font = '12px sans-serif'; ctx.fillStyle = '#e4e4e4'; if (gHours !== 0) { ctx.fillRect(0, Math.floor((vOff + graphH) * 0.5), page_width, 1); } ctx.fillRect(0, vOff - 1, page_width, 1); ctx.fillStyle = '#505050'; ctx.textAlign = 'right'; ctx.fillText(maxCount, xOff - 4, vOff + 1); // left border ctx.fillStyle = '#d4d4d4'; ctx.fillRect(xOff - 2, 0, 1, graphH); ctx.fillStyle = '#ffffff'; ctx.fillRect(xOff - 1, 0, 1, graphH); // bottom border ctx.fillStyle = '#d4d4d4'; ctx.fillRect(0, graphH, page_width, 1); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, graphH + 1, page_width, 1); if (gHours === 0) { if (pastReviews) { gOff = xOff; height = graphH - vOff; for (ti = 0; ti < 3; ++ti) { count = pastReviews[ti][0]; if (count > 0) { width = Math.ceil(count / maxCount * (page_width - xOff)); drawBarRect(ctx, gOff, vOff, width, height, api_colors[ti]); gOff += width; } } } } else { for (i = 1; i < 4; ++i) { xP = Math.floor(i / 3 * (page_width - 2)); if (i === 3) { xP += 1; } else if (page_width > 1100) { --xP; } ctx.fillStyle = '#e4e4e4'; ctx.fillRect(xP, 0, 1, graphH_canvas); ctx.fillStyle = '#505050'; ctx.fillText(String(hrsFrac * i), xP - 2, 0); } for (time = 0; time < times.length; time++) { counts = times[time]; if (counts) { hOff = 0; currentType = 0; if (counts[0][1] || counts[1][1] || counts[2][1]) { currentType = -1; } else if (counts[0][2] || counts[1][2] || counts[2][2]) { currentType = -2; } if (currentType) { drawBar(ctx, time - 1, graphH - vOff, 0, 'rgba(' + (currentType === -1 ? '255, 255, 255' : '0, 0, 0') + ', 0.33)', tFrac); } for (ti = 0; ti < 3; ++ti) { count = counts[ti][0]; if (count > 0) { height = Math.ceil(count / maxCount * (graphH - vOff)); drawBar(ctx, time - 1, height, hOff, api_colors[ti], tFrac, currentType); hOff += height; } } if (options.enable_arrows) { drawArrow(ctx, currentType, xOff, time, tFrac); } } } } } } function displayUpdateTime(date) { var isoString, utcString; isoString = date.toISOString(); utcString = isoString.replace(/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})\.\d{3}Z/, '$1 $2 UTC'); $('section#r-timeline time').attr('datetime', isoString); $('section#r-timeline time').attr('title', 'Updated: ' + utcString); $('section#r-timeline time').html(utcString); } function initCanvas() { var reviewHours = Math.ceil(firstReview / 60 / 60 / 6) * 6; $('div#r-timeline-loading').hide(); $('#r-timeline').show(); if (reviewHours > gHours) { gHours = reviewHours; $('#g-range').attr('value', gHours); // set slider $('#g-range').trigger('change'); // update slider text / draw } drawCanvas(); // need to init hover, even if redraw displayUpdateTime(new Date(parseInt(localStorage.getItem(localStoragePrefix + 'cacheTime'), 10))); } function appendError(newError) { var error = $('span#r-timeline-loading-error').html(); if (!error) { error = 'Error: '; } else { error += ', '; } error += newError; $('span#r-timeline-loading-error').html(error); } function ajaxError(xhr, ajaxOptions, thrownError) { appendError(thrownError); } // Load data function addData(data) { var itemIdx, response, myLevel, firstItem, typeIdx, maxSeconds, item, stats, availableAt, tDiff, timeIdx, timeTable; response = data.requested_information; if (!response) { if (data.error) { if (data.error.code === "user_not_found") { appendError('badApiKey'); localStorage.removeItem(localStoragePrefix + 'apiKey'); // remove invalid key return; } } appendError('badResponse'); return; } if (response.general) { response = response.general; } myLevel = data.user_information.level; firstItem = response[0]; typeIdx = firstItem.kana ? 2 : firstItem.important_reading ? 1 : 0; maxSeconds = 60 * 60 * max_hours; for (itemIdx = 0; itemIdx < response.length; itemIdx++) { item = response[itemIdx]; stats = item.user_specific; if (stats && !stats.burned) { availableAt = stats.available_date; tDiff = availableAt - start_time; if (tDiff < maxSeconds) { if (tDiff < firstReview) { firstReview = tDiff; } timeIdx = tDiff < 1 ? -1 : Math.round(tDiff / 60 / tRes) + 1; if (tDiff < 0) { if (!pastReviews) { pastReviews = []; // init object } timeTable = pastReviews; availableAt = 0; // clear for past reviews } else { if (!times[timeIdx]) { times[timeIdx] = []; // init object } timeTable = times[timeIdx]; } if (!timeTable[0]) { timeTable[0] = [0, 0, 0]; // 0:radical [0:total, 1:current, 2:burn] timeTable[1] = [0, 0, 0]; // 1:kanji timeTable[2] = [0, 0, 0]; // 2:vocab timeTable[3] = availableAt; // 3:time } else if (availableAt < timeTable[3]) { timeTable[3] = availableAt; } timeTable[typeIdx][0]++; // add item to r0/k1/v2 bin total if (typeIdx < 2 && item.level === myLevel && stats.srs === 'apprentice') { timeTable[typeIdx][1]++; // increment current } else if (stats.srs === 'enlighten') { timeTable[typeIdx][2]++; // increment burn } } } } ajaxCompletedCount++; if (ajaxCompletedCount === api_calls.length && times && times.length > 0) { localStorage.setItem(localStoragePrefix + 'reviewCache', JSON.stringify(times)); localStorage.setItem(localStoragePrefix + 'pastCache', JSON.stringify(pastReviews)); localStorage.setItem(localStoragePrefix + 'cacheTime', curr_date.getTime()); initCanvas(); } else { $('span#r-timeline-loading-count').html(ajaxCompletedCount + '/' + api_calls.length); if (ajaxCompletedCount >= api_calls.length) { appendError('noData'); } } } function insertTimeline() { var apiKey, cacheExpires, ext, idx, counts; ajaxCompletedCount = 0; apiKey = localStorage.getItem(localStoragePrefix + 'apiKey'); if (apiKey && apiKey.length === 32) { $('section.review-status').before('<div id="r-timeline-loading">Reviews Timeline Loading: <span id="r-timeline-loading-count"></span> <span id="r-timeline-loading-error" style="color:red"></span></div><section id="r-timeline" style="display: none;"><h4>Reviews Timeline</h4><a class="help">?</a> <a class="reload" title="reload (clear timeline cache)">R</a> <time class="timeago"></time><form id="graph-form"><label><span id="g-timereviews"></span> reviews <span id="g-timeframe">in ' + gHours + ' hours</span> <input id="g-range" type="range" min="0" max="' + max_hours + '" value="' + gHours + '" step="6" name="g-ranged"></label></form><div id="graph-tip" style="display: none;"></div><canvas id="c-timeline" height="' + graphH_canvas + '"></canvas></section>'); displayUpdateTime(new Date()); // normally overwritten later. workaround for 'Real Times' loading before data download. $('span#r-timeline-loading-count').html(ajaxCompletedCount + '/' + api_calls.length); try { times = JSON.parse(localStorage.getItem(localStoragePrefix + 'reviewCache')); pastReviews = JSON.parse(localStorage.getItem(localStoragePrefix + 'pastCache')); } catch (ignore) {} //~ if (times && pastReviews) { if (times) { cacheExpires = localStorage.getItem(localStoragePrefix + 'cacheTime'); if (cacheExpires && curr_date - cacheExpires > 60 * 60 * 1000) { times = null; } } //~ if (!times || !pastReviews || times.length === 0) { if (!times || times.length === 0) { times = null; pastReviews = null; localStorage.setItem(localStoragePrefix + 'reviewCache', null); localStorage.setItem(localStoragePrefix + 'pastCache', null); times = []; firstReview = Number.MAX_VALUE; for (ext = 0; ext < api_calls.length; ext++) { $.ajax({ type: 'get', url: '/api/v1.2/user/' + apiKey + '/' + api_calls[ext], success: addData, error: ajaxError }); } } else { for (idx = 0; idx < times.length; idx++) { counts = times[idx]; if (counts) { firstReview = counts[3] - start_time; break; } } setTimeout(initCanvas, 0); } $('a.help').click(function () { alert('Reviews Timeline - Displays your upcoming reviews\nY-axis: Number of reviews\nX-axis: Time (scale set by the slider)\n\nThe number in the upper left shows the maximum number of reviews in a single bar.\nWhite-backed bars indicate that review group contains radicals/kanji necessary for advancing your current level.'); }); $('a.reload').click(function () { if (confirm('Reviews Timeline: Reload Confirmation\n\nClick OK to clear the cache and refresh the page.\n\nWarning:\nExcessive API requests may be blocked by the server.') === true) { localStorage.removeItem(localStoragePrefix + 'reviewCache'); document.location.reload(); } }); $('#g-range').change(function () { gHours = $(this).val(); if (gHours < 6) { gHours = pastReviews ? 0 : 3; } $('#g-timeframe').text(gHours === 0 ? 'right now' : 'in ' + gHours + ' hours'); drawCanvas(true); }); } else { alert('Hang on! We\'re grabbing your API key for the Reviews Timeline. We should only need to do this once.'); document.location.pathname = '/account'; } } // from: https://gist.githubusercontent.com/arantius/3123124/raw/grant-none-shim.js function addStyle(aCss) { var head, style; head = document.getElementsByTagName('head')[0]; if (head) { style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.textContent = aCss; head.appendChild(style); return style; } return null; } styleCss = '\n' + '#graph-tip {\n' + ' padding: 2px 8px;\n' + ' position: absolute;\n' + ' color: #eeeeee;\n' + ' background-color: rgba(0,0,0,0.5);\n' + ' border-radius: 4px;\n' + ' pointer-events: none;\n' + ' font-weight: bold;\n' + '}\n' + 'section#r-timeline {\n' + ' overflow: hidden;\n' + ' margin-bottom: 0px;\n' + ' height: ' + (options.enable_arrows ? '130' : '117') + 'px;\n' + '}\n' + 'form#graph-form {\n' + ' float: right;\n' + ' margin-bottom: 0px;\n' + ' min-width: 50%;\n' + ' text-align: right;\n' + '}\n' + 'section#r-timeline h4 {\n' + ' clear: none;\n' + ' float: left;\n' + ' height: 20px;\n' + ' margin-top: 0px;\n' + ' margin-bottom: 4px;\n' + ' font-weight: normal;\n' + ' margin-right: 12px;\n' + '}\n' + 'a.help, a.reload {\n' + ' font-weight: bold;\n' + ' color: rgba(0, 0, 0, 0.1);\n' + ' font-size: 1.2em;\n' + ' line-height: 0px;\n' + '}\n' + 'a.help:hover, a.reload:hover {\n' + ' text-decoration: none;\n' + ' cursor: help;\n' + ' color: rgba(0, 0, 0, 0.5);\n' + '}\n' + '@media (max-width: 767px) {\n' + ' section#r-timeline h4 {\n' + ' display: none;\n' + ' }\n' + '}\n' + '.dashboard section.review-status ul li time {\n' + ' white-space: nowrap;\n' + ' overflow-x: hidden;\n' + ' height: 1.5em;\n' + ' margin-bottom: 0;\n' + '}\n'; if (document.location.pathname === "/account") { (function () { var apiKey, alreadySaved; apiKey = $('input[placeholder="Key has not been generated"]').val(); if (apiKey) { alreadySaved = localStorage.getItem(localStoragePrefix + 'apiKey'); localStorage.setItem(localStoragePrefix + 'apiKey', apiKey); console.log('WaniKani Timeline Updated API Key: ' + apiKey); if (!alreadySaved) { document.location.pathname = '/dashboard'; } } }()); } else { page_width = $('.span12 header').width(); if (page_width) { tRes = Math.round(1 / (page_width / 1170 / 15)); insertTimeline(); } addStyle(styleCss); } console.log('WaniKani Timeline: script load end'); }());
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址