WaniKanify

Firefox version of chedkid's excellent Chrome app

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        WaniKanify
// @namespace   wanikani
// @description Firefox version of chedkid's excellent Chrome app
// @include     *
// @version     1.4.0
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @require     http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js
// ==/UserScript==

// Current format version of the vocab database
var FORMAT_VER = 2;

// Lock to ensure only one download at once
var downloading = false;

/* Main */
window.addEventListener ("load", function () {

    GM_registerMenuCommand("Wanikanify: Run", tryRun);
    GM_registerMenuCommand("Wanikanify: Refresh vocabulary", tryRefreshVocabulary);
    GM_registerMenuCommand("Wanikanify: Enable auto-run", promptAutoRun);
    GM_registerMenuCommand("Wanikanify: Set API key", promptApiKey);

    if (GM_getValue("autoRun") == 1)
        run(false);

}, false);

/**
 * User interface
 */

/* Run script */
function tryRun() {
    var apiKey = getApiKey();
    if (apiKey != undefined)
        run(false);
}

/* Refresh vocabulary */
function tryRefreshVocabulary() {
    var apiKey = getApiKey();
    if (apiKey != undefined)
        downloadVocab();
}

/* Specifiy whether to run automatically */
function promptAutoRun() {
    var autoRun = parseInt(window.prompt("Enter 1 to enable auto-run and 0 to disable", GM_getValue("autoRun") ? 1 : 0));
    GM_setValue("autoRun", autoRun);
}

/* Specify the WaniKani API key */
function promptApiKey() {
    var apiKey;

    while (true) {
        apiKey = GM_getValue("apiKey");
        apiKey = window.prompt("Please enter your API key", apiKey ? apiKey : "");

        if (apiKey && !/^[a-fA-F0-9]{32}$/.test(apiKey))
            alert("That was not a valid API key, please try again");
        else
            break;
    }

    GM_setValue("apiKey", apiKey);
    GM_setValue("mustRefresh", 1);
}

/**
 * Runtime code
 */

function getApiKey() {
    var apiKey = GM_getValue("apiKey");

    if (apiKey == undefined)
        promptApiKey();

    return apiKey;
}

function run(refreshFirst) {
    // Ignore current vocab download
    downloading = false;

    // Get the current timestamp (in minutes)
    var currentTime = Math.floor(new Date().getTime() / 60000);

    // Get the time and the vocab format from when the vocab list was last refreshed
    var refreshTime = GM_getValue("refreshTime");
    var refreshFormat = GM_getValue("formatVer");

    // Hotfix: Allow script to enforce refresh
    var mustRefresh = GM_getValue("mustRefresh") == 1;
    GM_setValue("mustRefresh", 0);

    // See if we should to refresh. If a week has passed, it's probably a good idea anyway
    var shouldRefresh = mustRefresh || refreshFirst || (GM_getValue("vocab", "") == "") || (refreshFormat != FORMAT_VER)
    || (refreshTime == undefined) || (currentTime - refreshTime >= 10080);

    // No refresh needed, simply replace vocab...
    if (!shouldRefresh)
        replaceVocab();
    // Update vocab list if last refresh was too long ago (and replace afterwards), or old version
    else
        downloadVocab(true);
}

function downloadVocab(runAfter, wasManual) {
    if (downloading) {
        console.log("Attempted to download WaniKani data while already downloading");
        return;
    }

    var apiKey = GM_getValue("apiKey");
    if (apiKey == undefined)
        return;

    console.log("Downloading new vocab data...");
    downloading = true;

    GM_xmlhttpRequest({
        method: "GET",
        url: "http://www.wanikani.com/api/v1.2/user/" + apiKey + "/vocabulary/",
        onerror: function (response) {
            alert("Error while downloading WaniKani data. Please try again later.");
            downloading = false;
        },
        onload: function (response) {
            var json;

            try {
                json = JSON.parse(response.responseText);
            } catch (e) {
                alert("Unable to process WaniKani data. Please try again later.", e);
            }

            if (json) {
                if ("error" in json) {
                    alert("Error from WaniKani: " + json.error.message);
                    downloading = false;
                    return;
                }

                GM_setValue("refreshTime", Math.floor(new Date().getTime() / 60000));

                // Create the vocab map that will be stored
                var vocabMap = {};
                // Loop through all received words
                var vocabList = json.requested_information.general;

                for (var i in vocabList) {
                    var vocab = vocabList[i];

                    // Skip words not yet learned
                    if (!vocab.user_specific)
                        continue;

                    // Split multiple spellings
                    var meanings = vocab.meaning.split(", ");

                    // Conjugate verbs
                    var conjugations = [];
                    for (var m in meanings) {
                        var word = meanings[m];

                        // If a verb...
                        if (/^to /.test(word))
                        {
                            // Remove leading 'to'
                            meanings[m] = word.substr(3);

                            // Remove 'e' suffix for conjugations
                            if (word.slice(-1) == "e")
                                word = word.slice(0, -1);

                            if (!/ /.test(word))
                                conjugations.push(word + "ed", word + "es", word + "en", word + "es", word + "s", word + "ing");
                        }

                        // Not a verb, try plural
                        else if (word.length >= 3 && word.slice(-1) != "s")
                            conjugations.push(word + "s");
                    }
                    meanings.push.apply(meanings, conjugations);

                    // After updating the meanings,
                    for (var m in meanings) {
                        vocabMap[meanings[m]] = vocab.character;
                    }
                }

                String.prototype.hashCode = function() {
                    var hash = 0, i, char;
                    if (this.length == 0) return hash;
                    for (i = 0, l = this.length; i < l; i++) {
                        char  = this.charCodeAt(i);
                        hash  = ((hash<<5)-hash)+char;
                        hash |= 0; // Convert to 32bit integer
                    }
                    return hash;
                };

                // Update the new vocab list in the storage
                vocabJson = JSON.stringify(vocabMap);
                GM_setValue("vocab", vocabJson);
                if(!runAfter) {
                    if (vocabJson.hashCode() != GM_getValue("vocabHash")) {
                        alert("Successfully updated vocab!");
                        GM_setValue("vocabHash", vocabJson.hashCode());
                    }
                    // Only inform the user that the vocab is up to date if he manually requested to update it
                    else {
                        alert("Vocab already up to date");
                    }
                }
                GM_setValue("formatVer", FORMAT_VER);

                // Unlock download lock
                downloading = false;

                // If wanted, immediately run the script
                if (runAfter)
                    replaceVocab(vocabMap);
            }
        }
    });
}

function replaceVocab(vocabMap) {
    // No vocab map given, try to parse from stored JSON
    if (!vocabMap) {
        try {
            vocabMap = JSON.parse(GM_getValue("vocab"), {});
            if (!vocabMap || (vocabMap && jQuery.isEmptyObject(vocabMap)))
                throw 1;
        } catch (e) {
            alert("Error while parsing the vocab list; deleting it now. Please try again.");
            GM_setValue("vocab", "");
            return;
        }
    }

    console.log(vocabMap);
    console.log("Replacing vocab...");

    var replaceCallback = function(str) {
        if (vocabMap.hasOwnProperty(str.toLowerCase())) {
            var translation = vocabMap[str.toLowerCase()];
            return '<span class="wanikanified" title="' + str + '" data-en="' + str + '" data-jp="' + translation +
                '" onClick="var t = this.getAttribute(\'title\'); this.setAttribute(\'title\', this.innerHTML); this.innerHTML = t;">' + translation + '<\/span>';
        }

        // Couldn't replace anything, leave as is
        return str;
    };

    var nodes = $("body *:not(noscript):not(script):not(style)");

    // Very naive attempt at replacing vocab consisting of multiple words first
    nodes.replaceText(/\b(\S+?\s+\S+?\s+\S+?\s+\S+?)\b/g, replaceCallback);
    nodes.replaceText(/\b(\S+?\s+\S+?\s+\S+?)\b/g, replaceCallback);
    nodes.replaceText(/\b(\S+?\s+\S+?)\b/g, replaceCallback);
    nodes.replaceText(/\b(\S+?)\b/g, replaceCallback);

    console.log("Vocab replaced!"); 
}

/*
 * jQuery replaceText - v1.1 - 11/21/2009
 * http://benalman.com/projects/jquery-replacetext-plugin/
 * 
 * Copyright (c) 2009 "Cowboy" Ben Alman
 * Dual licensed under the MIT and GPL licenses.
 * http://benalman.com/about/license/
 */
(function($){$.fn.replaceText=function(b,a,c){return this.each(function(){var f=this.firstChild,g,e,d=[];if(f){do{if(f.nodeType===3){g=f.nodeValue;e=g.replace(b,a);if(e!==g){if(!c&&/</.test(e)){$(f).before(e);d.push(f)}else{f.nodeValue=e}}}}while(f=f.nextSibling)}d.length&&$(d).remove()})}})(jQuery);