您需要先安装一个扩展,例如 篡改猴、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.0 // @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 */ /* Almost all of this code is by Kyle Coburn aka kiko on WakiKani. It has been reformatted slightly and some minor changes made. */ (function () { 'use strict'; var tRes, counted, api_calls, api_colors, curr_date, start_time, gHours, graphH, styleCss, xOff, vOff, max_hours, times, pastReviews, firstReview, tFrac, page_width, options = {}; // options['12_hours_i'] = true; // uncomment for 12-hour AM/PM mode counted = 0; api_calls = ['radicals', 'kanji', 'vocabulary']; api_colors = ['#0096e7', '#ee00a1', '#9800e8']; curr_date = new Date(); start_time = curr_date.getTime() / 1000; gHours = 12; graphH = 88; xOff = 18; vOff = 16; max_hours = 72; // 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('second', 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); } } function drawCanvas(clear) { var totalCount, maxCount, graphTimeScale, ti, time, counts, total, ctx, gTip, pidx, canvasJQ, idx, rCount, showTime, minDiff, tDisplay, tDate, hours, mins, suffix, tText, currentType, hrsFrac, gOff, height, count, i, width, hOff, xP, canvas = document.getElementById('c-timeline'); 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]; } 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]; } 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); page_width = $('.span12 header').width(); } else { gTip = $('#graph-tip'); canvasJQ = $('#c-timeline'); canvas.addEventListener('mousemove', function (event) { 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(); rCount = counts[0] + counts[1] + counts[2]; showTime = counts[4] * 1000; minDiff = (showTime - new Date().getTime()) / 1000 / 60; if (minDiff < 90) { tDisplay = fuzzyMins(minDiff); } else { tDate = new Date(showTime); hours = tDate.getHours(); mins = tDate.getMinutes(); suffix = ''; if (options && options['12_hours_i']) { suffix = ' ' + (hours < 12 ? 'am' : 'pm'); hours %= 12; if (hours === 0) { hours = 12; } } if (hours < 10) { hours = '0' + hours; } if (mins < 10) { mins = '0' + mins; } tDisplay = hours + ':' + mins + suffix; } tText = tDisplay + '<br />' + pluralize('review', rCount); currentType = counts[3]; if (currentType) { tText += '<br /><em>'; tText += currentType === -1 ? 'current level' : 'burning'; tText += '</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 (event) { 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); ctx.fillStyle = '#d4d4d4'; ctx.fillRect(xOff - 2, 0, 1, graphH); ctx.fillStyle = '#ffffff'; ctx.fillRect(xOff - 1, 0, 1, graphH); if (gHours === 0) { if (pastReviews) { gOff = xOff; height = graphH - vOff; for (ti = 0; ti < 3; ++ti) { count = pastReviews[ti]; 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); 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 = counts[3]; 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]; if (count > 0) { height = Math.ceil(count / maxCount * (graphH - vOff)); drawBar(ctx, time - 1, height, hOff, api_colors[ti], tFrac, currentType); hOff += height; } } } } } } } function initCanvas() { var reviewHours = Math.ceil(firstReview / 60 / 60 / 6) * 6; if (reviewHours > gHours) { gHours = reviewHours; $('#g-timeframe').text(gHours); } if (firstReview > 3 * 60 * 60) { $('#g-range').attr('min', reviewHours); } $('#r-timeline').show(); $('section.review-status').css('border-top', '1px solid #fff'); drawCanvas(); } // 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 (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 = [0, 0, 0, 0, availableAt]; } timeTable = pastReviews; } else { timeTable = times[timeIdx]; } if (!timeTable) { times[timeIdx] = [0, 0, 0, 0, availableAt]; timeTable = times[timeIdx]; } else if (availableAt < timeTable[4]) { timeTable[4] = availableAt; } ++timeTable[typeIdx]; // add item to r0/k1/v2 bin if (timeTable[3] !== -1) { // change to give current level priority if (typeIdx < 2 && item.level === myLevel && stats.srs === 'apprentice') { timeTable[3] = -1; } else if (stats.srs === 'enlighten') { timeTable[3] = -2; } } } } } if (++counted === 3 && times && times.length > 0) { localStorage.setItem('reviewCache', JSON.stringify(times)); localStorage.setItem('pastCache', JSON.stringify(pastReviews)); localStorage.setItem('cacheExpiration', curr_date.getTime()); initCanvas(); } } } function insertTimeline() { var apiKey, cacheExpires, ext, idx, counts; apiKey = localStorage.getItem('apiKey'); if (apiKey && apiKey.length === 32) { $('section.review-status').before('<section id="r-timeline" style="display: none;"><h4>Reviews Timeline</h4><a class="help">?</a><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></section>'); try { times = JSON.parse(localStorage.getItem('reviewCache')); pastReviews = JSON.parse(localStorage.getItem('pastCache')); } catch (ignore) {} if (times && pastReviews) { cacheExpires = localStorage.getItem('cacheExpiration'); if (cacheExpires && curr_date - cacheExpires > 60 * 60 * 1000) { times = null; } } if (!times || !pastReviews || times.length === 0) { times = null; pastReviews = null; localStorage.setItem('reviewCache', null); localStorage.setItem('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 }); } } else { for (idx = 0; idx < times.length; idx++) { counts = times[idx]; if (counts) { firstReview = counts[4] - 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. White-backed bars indicate that review group contains radicals/kanji necessary for advancing your current level.'); }); $('#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' + ' border-bottom: 1px solid #d4d4d4;\n' + ' height: 113px;\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 {\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 {\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('apiKey'); localStorage.setItem('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'); }());