您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allows you to lock a set number of items from your review queue so you keep on top of the rest.
当前为
// ==UserScript== // @name WaniKani Lock Script // @namespace https://www.wanikani.com // @author Doncr // @description Allows you to lock a set number of items from your review queue so you keep on top of the rest. // @version 0.0.4 // @include *://www.wanikani.com/* // @grant none // @run-at document-body // ==/UserScript== (function (xhr) { var localStorageConfigKey = "lockScriptCache"; var localStorageAPIKeyKey = "apikeyv2"; function getConfig() { var config = localStorage.getItem(localStorageConfigKey); if (config) { return JSON.parse(config); } else { return { availableAt: {}, lockCount: 0, }; } } function setConfig(config) { localStorage.setItem(localStorageConfigKey, JSON.stringify(config)); } function getAPIKey() { return localStorage.getItem(localStorageAPIKeyKey); } function reloadCache() { var apiKey = getAPIKey(); if (apiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) { // Clear out any availability data from the cache. var config = getConfig(); config.availableAt = {}; delete config.availableUpdatedAt; setConfig(config); updateAssignmentsCache(); } } function setAPIKey() { var apiKey = window.jQuery('#lockScriptAPIKey').val(); if (apiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) { if (apiKey != getAPIKey()) { localStorage.setItem(localStorageAPIKeyKey, apiKey); } reloadCache(); } else if (apiKey.match(/^[0-9a-f]{32}$/)) { alert("It looks like you entered the API Version 1 key. You need to use the API Version 2 key."); } else { alert("Invalid API key format. You need to use the API Version 2 key."); } } function updateAssignmentsCache() { var cache = getConfig(); if (getAPIKey()) { var uri = "https://www.wanikani.com/api/v2/assignments"; if (cache.availableUpdatedAt) { uri = uri + "?updated_after=" + cache.availableUpdatedAt; } while (uri) { var response = jQuery.ajax({ headers: { "Authorization": "Token token=" + getAPIKey() }, dataType: "json", async: false, url: uri }); var json = response.responseJSON; if (json.data_updated_at) { cache.availableUpdatedAt = json.data_updated_at; } json.data.forEach(function (datum) { var key = datum.data.subject_id.toString(); var avail = datum.data.available_at; var srs_stage = datum.data.srs_stage; var hidden = datum.data.hidden; if ((avail != null) && (srs_stage > 0) && (srs_stage < 9) && (!hidden)) { var value = Date.parse(avail) / 1000; cache.availableAt[key] = { "t": value, "l": srs_stage }; } else { delete cache.availableAt[key]; } }); uri = json.pages.next_url; } // Reduce lockCount if there aren't that many items left in the real review queue. var now = Date.now() / 1000; var realQueueSize = Object.values(cache.availableAt).filter(function (item) { return item.t < now; }).length; if (cache.lockCount > realQueueSize) { cache.lockCount = realQueueSize; } window.managedToUpdateLockCache = true; } setConfig(cache); } function modifyReviewQueue(queueText) { var queue = JSON.parse(queueText); var config = getConfig(); var availableAt = config.availableAt; var lockCount = parseInt(config.lockCount); if (lockCount > 0) { var queueByTime = queue.map(function (item) { return item.id }).sort(function (a, b) { var sortMethod1 = availableAt[a].t - availableAt[b].t; if (sortMethod1 != 0) { return sortMethod1; } return a - b; }); queueByTime = queueByTime.slice(lockCount); var queueByTimeKeys = {}; queueByTime.forEach(function (item) { queueByTimeKeys[item] = true; }); queue = queue.filter(function (item) { return queueByTimeKeys[item.id.toString()]; }); window.lockScriptFilter = true; window.lockScriptLockCount = config.lockCount; } return JSON.stringify(queue); } var originalResponseText = Object.getOwnPropertyDescriptor(xhr, 'responseText'); Object.defineProperty(xhr, 'responseText', { get: function () { // Use the original responseText property accessor so we can get the // actual response from WK. var responseText = originalResponseText.get.apply(this); if (this.responseURL == "https://www.wanikani.com/review/queue") { updateAssignmentsCache(); responseText = modifyReviewQueue(responseText); } return responseText; } }); // DOM fiddling function graphMarkup() { return '' + '<div class="pure-g-r">' + ' <div class="pure-u-1">' + ' <fieldset>' + ' <legend>Lock Script options</legend>' + ' API Key V2: <input style="font-family: monospace; width: 300px;" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" id="lockScriptAPIKey"> <input type="button" value="Set API key" id="setAPIKey"> <input type="button" value="Reload cache" id="reloadCache"><br>(See <a href="https://www.wanikani.com/settings/account">Account settings</a> to get your API Version *2* key).<br>' + ' Locked: <input id="lockScriptLockCount"><br>' + ' <input type="button" id="lockScriptSetOptions" value="Save options"><br>' + ' </fieldset>' + ' </div>' + '</div>'; } function lockCountMarkup(numLocked) { return '' + '<span class="review-lock-count" style="background-color: black; color: white; display: inline-block; line-height: 3em; padding-left: 16px;">' + ' <i style="margin-right: 8px" class="icon-lock"></i>' + ' <span>' + numLocked + '</span>' + '</span>'; } document.body.addEventListener('DOMSubtreeModified', function (event) { var attrs = event.target.attributes; if (window.location.pathname == "/review") { if ((event.target.tagName == 'DIV') && (attrs.class && attrs.class.value == 'review-stats-value')) { if (!window.lockMarkupAdded) { window.lockMarkupAdded = true; updateAssignmentsCache(); window.jQuery('#review-stats').parent('.pure-g-r').after(graphMarkup); var config = getConfig(); window.jQuery('#setAPIKey').click(setAPIKey); window.jQuery('#reloadCache').click(reloadCache); var availableCount = 0; var now = Date.now() / 1000; Object.keys(config.availableAt).forEach(function (key) { if (config.availableAt[key].t < now) { availableCount++; } }); window.jQuery('#lockScriptAPIKey').val(getAPIKey()); if ( window.managedToUpdateLockCache) { window.jQuery('#lockScriptLockCount').val(config.lockCount); var numLocked = config.lockCount; var numUnlocked = availableCount - config.lockCount; window.jQuery('#review-queue-count').before(lockCountMarkup(numLocked)); window.jQuery('#review-queue-count').text(numUnlocked); } window.jQuery('#lockScriptSetOptions').click(function () { var config = getConfig(); var lockCount = window.jQuery('#lockScriptLockCount').val(); config.lockCount = lockCount; setConfig(config); }); } } } if (window.location.pathname == "/review/session") { if ((event.target.tagName == 'DIV') && (attrs.id && attrs.id.value == 'loading') && (attrs.style && attrs.style.value == "display: none;")) { if (window.lockScriptFilter) { window.jQuery('#stats').prepend("<i class='icon-lock'></i><span>" + window.lockScriptLockCount + "</span>"); } } } }, false); })(XMLHttpRequest.prototype);