WaniKani Search Result Annotator

Show lock/unlock and SRS state in the WaniKani search result. Inspired by WaniKani Item Annotator by jeshuamorrissey and mempo.

目前为 2017-11-03 提交的版本。查看 最新版本

// ==UserScript==
// @name       WaniKani Search Result Annotator
// @namespace   carina
// @description Show lock/unlock and SRS state in the WaniKani search result. Inspired by WaniKani Item Annotator by jeshuamorrissey and mempo.
// @author     carina
// @version    1.0.1
// @include https://www.wanikani.com*
// @include https://www.wanikani.com/dashboard*
// @include https://www.wanikani.com/account*
// @include http://www.wanikani.com/radical*
// @include http://www.wanikani.com/kanji*
// @include http://www.wanikani.com/vocabulary*
// @include http://www.wanikani.com/account*
// @include https://www.wanikani.com/radical*
// @include https://www.wanikani.com/kanji*
// @include https://www.wanikani.com/vocabulary*
// @grant       none
// ==/UserScript==

/* #### API ACCESS ####################################################################*/
function retrieveAPIkey() {
    var apiKey = document.getElementById('user_api_key').value;
    alert('API key was set to: ' + apiKey);
    if (apiKey) {
        return apiKey;
    }
}

function apiURL(target) {
    return 'https://www.wanikani.com/api/user/' + apiKey + '/' + target;
}

/* #### API ACCESS ####################################################################*/
function retrieveAPIkey() {
    var apiKey = document.getElementById('user_api_key').value;
    alert('API key was set to: ' + apiKey);
    if (apiKey) {
        return apiKey;
    }
}

function apiURL(target) {
    return 'https://www.wanikani.com/api/user/' + apiKey + '/' + target;
}

var apiKey = $.jStorage.get('WSRA_apiKey');

if (apiKey === null) { //not initialized yet
    console.log('#### no apiKey found');

    if (window.location.href.indexOf('account') != -1) {
        debugger;
        apiKey = "" + retrieveAPIkey();
        console.log('@@@@@' + apiKey);
        $.jStorage.set('WSRA_apiKey', apiKey);
    } else {
        var okcancel = confirm('WaniKani Search Result Annotator has no API key entered.\nPlease press OK to go to your settings page and retrieve your API key.');
        if (okcancel == true) {
            window.location = 'https://www.wanikani.com/settings/account';
            return;
        }
    }
}

console.log('#### apiKey is: ' + apiKey);

/* #### SEARCH RESULT OBSERVATION #####################################################*/
// observes changes to search result list on demand
var searchResultObserver = new MutationObserver(annotateSearchResult);

//listen for new searches
$("#query").on('keyup', function (e) {
    if (e.keyCode == 13) {
        //user has pressed enter in the search form: listen for a change in the search result div so that we can update the search result
        try {
            startSearchResultObservation();
        } catch (e) {
            console.log('an error occurred while running startSearchResultObservation');
        }
    }
});

function startSearchResultObservation() {
    searchResultObserver.observe($('.search-results')[0], {
        subtree: true,
        childList: true,
        attributes: false
    });
}

function stopSearchResultObservation() {
    // Stop observing changes
    searchResultObserver.disconnect();

    // Empty the queue of records
    searchResultObserver.takeRecords();
}

/* #### SEARCH RESULT ANNOTATION ######################################################*/
function annotateSearchResult() {
    console.log('#### search result change was detected, going to run WaniKani Search Result Annotator');

    //we already recognized a change, no need to keep observing the search result for now
    stopSearchResultObservation();

    loadAndAnnotateItems('radicals');
    loadAndAnnotateItems('kanji');
    loadAndAnnotateItems('vocabulary');
}


function loadAndAnnotateItems(target) {
    console.log('@@@ loading data for: ' + target);

    // Load the API data.
    $.get(apiURL(target), function (xhr) {
        // Build up an item mapping from Kanji --> Information
        var itemMapping = {};

        // Get the actual request information. If the target is vocabulary, for some reason
        // we have to got an additional level into 'request_information.general'. This is
        // probably to account for specialised vocab which will be added later.
        var information = xhr.requested_information;
        if (target === 'vocabulary') {
            information = information.general;
        }

        for (var i in information) {
            var item = information[i];

            // Extract the character (Kanji) from the item.
            var character = item.character;

            // If we are looking at radicals, use the meaning instead (convert the meaning to
            // the 'user friendly' format).
            if (target === 'radicals') {
                character = item.meaning.toLowerCase();
            }

            // Get the SRS level from the item. The 'user_specific' object will be `null` if the item
            // hasn't been unlocked yet. In this case, just set the SRS level to `null`.
            var srs = null;
            if (item.user_specific) {
                srs = item.user_specific.srs;
            }

            // Build the mapping for this character.
            itemMapping[character] = {
                'srs': srs
            };
        }

        // Actually do stuff with this mapping.
        annotateItems(itemMapping, target);
    });
}

/**
 * Annotate the elements (show locked/unlocked state and SRS level). Takes as input information from
 * the WK API as a mapping from Japanese Element --> Object. In this case, the
 * object need only contain the SRS level of the element.
 */
function annotateItems(itemMapping, target) {
    // Find all characters within the search result
    var elements = $('.search-results .character-item');

    var i = 0;
    for (i = 0; i < elements.size(); i++) {
        var element = elements[i];

        //if the element doesn't match the target type: ignore it
        //use only start of the target string because of "radicals" vs "radical"
        if (!element.id.startsWith(target.substr(0, 4))) {
            continue;
        }

        // The japanese value to look up in the item mapping is the text of this element.
        var japanese = element.querySelector('.character').textContent;

        // If we happen to be looking at radicals, some of them use pictures instead. It is
        // simpler to use the radical meaning in this case (as there is only one meaning).
        // The meaning is stored in the last list element within the element (for some reason
        // there is a &nbsp; list element first).
        if (target === 'radicals') {
            var radicalLink = element.querySelector('a').getAttribute('href');
            japanese = radicalLink.slice(radicalLink.lastIndexOf('/') + 1);
        }

        var itemInfo = itemMapping[japanese];

        //mark as locked if there is no SRS level yet
        if (typeof itemInfo == "undefined" || typeof itemInfo.srs !== "string") {
            $(element).addClass("locked");
        }
    }
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址