Skip to content

Instantly share code, notes, and snippets.

@Konatopic
Last active April 2, 2018 08:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Konatopic/8b1a6f6dbf9ea66ee4f50c2d35908518 to your computer and use it in GitHub Desktop.
Save Konatopic/8b1a6f6dbf9ea66ee4f50c2d35908518 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Homophone Explorer
// @namespace com.konatopic.hpx
// @version 0.9.1
// @description Finds homophones on Wanikani.com on each vocabulary page and during lesson and review sessions.
// @author Konatopic
// @grant GM_setValue
// @grant GM_getValue
// @include /^http(s)?://www\.wanikani\.com/vocabulary//
// @include /^http(s)?://www\.wanikani\.com/level/[0-9]+/vocabulary//
// @include /^http(s)?://www\.wanikani\.com/review/session/
// @include /^http(s)?://www\.wanikani\.com/lesson/session/
// @homepageURL https://gist.github.com/Konatopic/8b1a6f6dbf9ea66ee4f50c2d35908518
// ==/UserScript==
// TODOs #########################################################
// =============================== CONSTANTS =================================== //
var MAX_LEVEL = 60; // as of this version
var KEY_NAMES = { // names of entries as stored by HPX in storage - best not to change these after first use
API_KEY:'APIKey',
DATA:'data',
USER_SETTINGS:'userSettings',
LAST_UPDATED:'lastUpdated'
};
var SETTINGS_URL = 'https://www.wanikani.com/settings/account';
var API_VERSION = "v1.4"; // built with version 1.4
var API_REQUEST_TEMPLATE = {
VOCAB_LIST:'https://www.wanikani.com/api/{VERSION_NUMBER}/user/{USER_API_KEY}/vocabulary/{levels}'
};
// =============================== GLOBALS ====================================== //
var minUpdateInterval = 60;// minimum time between each automatic refresh in minutes
var lastUpdated;
// Some common names for the API Key variable defined some other script authors
var commonAPIKeyNames = ['apiKey'];
var hpx, // app-controller
ui; // ui-controller
// =============================== FUNCTIONS ==================================== //
// Wanikani uses jQuery (albeit possibly incomplete) -- might as well take advantage of it
(function checkJQuery(){
if(typeof jQuery !== 'undefined') {
(function(){
hpx = new HPX(); // Entry point
})();
} else {
setTimeout(function(){checkJQuery();},100);
}
})();
// App controller
function HPX(){
console.log('HPX initializing; Using jQuery version: ' + jQuery.fn.jquery + '. Caution: minified library may be incomplete');
var APIKey,
thisUpdating = false, // if this instance is updating
updateTimeoutID = 0.1; // setTimeout only returns integers
var vocabDB, // from API_REQUEST_TEMPLATE.VOCAB_LIST
requestedLevels;
var comparisonVocab,
comparisonReadings = [];
var userInformation,
vocabList;
/* eg.,
[0]{reading: 'あたり', vocabs:[[0]{name:"辺り",meaning:"area"... }]}
*/
var homophones = [];
// Clean up if user navigates away
window.onbeforeunload = function(e){
if(thisUpdating){
setUpdatingFlag(false);
}
};
// Load previous data if available
loadDataFromLocal();
// set up to listener for a new comparison vocab & reload & reloadapi
$(document).on('HPX:vocabUpdate',function(e,data){
if(data && data.exists){
comparisonVocab = data.comparisonVocab;
createHomophoneList();
ui.displayHomophones(homophones);
}
return false;
}).on('HPX:reloadRequest',function(e){
// only allow force request after an autoUpdate has been called previously
if(Number.isInteger(updateTimeoutID)){
clearTimeout(updateTimeoutID);
update(true);
}
return false;
}).on('HPX:reloadAPIRequest',function(e){
findAPIKey(function(key){
if(typeof key === 'string'){
APIKey = key;
}
},true);
});
// Setup UI
if($(location).attr('href').search(/^http(s)?:\/\/www\.wanikani\.com\/review\/session/) === 0 ||
$(location).attr('href').search(/^http(s)?:\/\/www\.wanikani\.com\/lesson\/session/) === 0){
ui = new UISessionPage();
} else {
ui = new UIPage();
}
ui.setStatus('INIT');
// Let's look for the API Key
findAPIKey(function(key){
if(typeof key === 'string'){
// returned valid key
APIKey = key;
autoUpdate(); // only call function once!!
} else {
console.log('Cannot find API Key anywhere. Please manually enter your by executing "localStorage.setItem(\''+
commonAPIKeyNames[0] +
'\', API_KEY);" in your developer console while on any wanikani.com page, where API_KEY is your 32 character API Key. Please report this to the developer.');
}
});
// schedules updates
function autoUpdate(forceUpdate){
var _lastUpdated = getLastUpdated(),
timeSinceUpdate,
timeUntilUpdate,
randomTime;
// add Math.random time to the scheduled update - javascript Chrome and Firefox extensions are probably single threaded but just to make sure
// this is so that when more than one injected tab is open, they do not all update at the same time
randomTime = Math.random() * 10000; // U(0,lim->10) seconds
// set these two variables first
if(typeof _lastUpdated === 'number'){
timeSinceUpdate = new Date().getTime() - _lastUpdated;
timeUntilUpdate = minUpdateInterval * 60 * 1000 - timeSinceUpdate + randomTime;
}
// only allow server updates on active tab
if (pageIsHidden()){
if(typeof _lastUpdated === 'number'){
if(timeSinceUpdate < minUpdateInterval * 60 * 1000 && timeSinceUpdate >= 0){
localCreateAndDisplay(); // but do allow it to do a local update
console.log('Scheduled autoUpdate in '+Math.floor(timeUntilUpdate/60000)+' minutes, ' + timeUntilUpdate%60000/1000+' seconds.');
return (updateTimeoutID = setTimeout(function(){autoUpdate();},timeUntilUpdate)); // and schedule an autoupdate as if it were an active tab
}
}
console.log('Scheduled autoUpdate in 3 seconds.');
return (updateTimeoutID = setTimeout(function(){autoUpdate();},3000)); // try again in 3 seconds
}
// else in the active tab, schedule is a server update
if(typeof _lastUpdated === 'number'){
if(timeSinceUpdate > minUpdateInterval * 60 * 1000){
// time for an update
update();
} else {
if(timeUntilUpdate > 2147483647){
// some funny business huh? - the user probably doesn't want to stick around for 24.8 days for page to update
update();
} else {
// schedule update
console.log('Scheduled autoUpdate in '+Math.floor(timeUntilUpdate/60000)+' minutes, ' + timeUntilUpdate%60000/1000+' seconds.');
updateTimeoutID = setTimeout(function(){autoUpdate();},timeUntilUpdate);
// reload from cache because another instance of HPX could have updated the cache
localCreateAndDisplay();
}
}
} else {
// first run
update();
}
}
// checks sessionStorage for updating flag. Useful when more than one injected tab is open
function isUpdating(){
var res = sessionStorage.getItem('HPX');
return (res !== null && JSON.parse(res).updating) ? true : false; // return updating status or false during first run
}
// updates from server immediately and sets up autoUpdate()
function update(forceReload){
if(thisUpdating){
return; // already updating in this instance
} else if(isUpdating() && !(typeof forceReload === 'boolean' && forceReload)){
// try again in 3 seconds
updateTimeoutID = setTimeout(function(){autoUpdate();},3000);
ui.setStatus('UPDATING_OTHER_INSTANCE');
console.log("Update queued. Retrying in 3 seconds");
return;
}
setUpdatingFlag(true);
ui.setStatus('UPDATING');
ui.toggleResetButton(false);
loadListFromServer(function(success,data){
ui.toggleResetButton(true);
setUpdatingFlag(false);
if(success){
userInformation = data.user_information;
vocabList = data.requested_information;
console.log('Updated cache');
// update lastUpdated
lastUpdated = new Date().getTime();
// schedule next update
autoUpdate();
ui.setStatus('IDLE');
} else {
console.log(data);
console.log('Problem connecting to server. Retrying in 3 seconds');
// schedule next update in 3 seconds
updateTimeoutID = setTimeout(function(){autoUpdate();},3000);
ui.setStatus('CONNECTION_ERROR');
}
});
}
// Sets HPX.updating flag in sessionStorage to prevent other HPX instances from updating
function setUpdatingFlag(_updating){
thisUpdating = _updating;
sessionStorage.setItem('HPX',JSON.stringify({updating:_updating}));
}
function localCreateAndDisplay(){
loadDataFromLocal();
createHomophoneList();
ui.displayHomophones(homophones);
}
// returns lastUpdated from GM_getValue
function getLastUpdated(){
var _lastUpdated = GM_getValue(KEY_NAMES.LAST_UPDATED);
return (typeof _lastUpdated !== 'undefined' ? _lastUpdated:undefined); // return time lastUpdated or undefined during first run
}
// finds the reading for the current vocab - don't really trust the reading on the page - the layout could've been altered by other scripts
function getComparisonReadings(){
for (var i = 0;i < vocabList.length; i++){
if(vocabList[i].character === comparisonVocab){
comparisonReadings = splitReadings(vocabList[i].kana);
console.log('Found ' + comparisonReadings.length + ' comparison readings found for ' + comparisonVocab + ': "'+vocabList[i].kana+'"');
return;
}
}
console.log('No comparison readings found for ' + comparisonVocab);
}
function createHomophoneList(){
console.log('Creating homophones list using comparator: ' + comparisonVocab);
var currentReadings = [];
// check if comparisonVocab exists and vocabList has been defined
if(typeof comparisonVocab === 'undefined' || !vocabList){
homophones = [];
return false;
}
getComparisonReadings();
// prepare homophones array
for (var k = 0; k < comparisonReadings.length; k++){
homophones[k] = {
reading:comparisonReadings[k],
vocabs:[]
};
}
// look through entire vocabList to find matching readings
for (var vocabIndex = 0; vocabIndex < vocabList.length; vocabIndex++){
currentReadings = splitReadings(vocabList[vocabIndex].kana);
// make separate list for each reading - most of the time there will only be one
for (var comparisonIndex = 0; comparisonIndex < comparisonReadings.length; comparisonIndex++){
// compare all comparison readings with each reading of the current vocab
for (var i = 0; i < currentReadings.length; i++){
if(currentReadings[i] === comparisonReadings[comparisonIndex]){
// found one - probably if it's not the same as the comparison
homophones[comparisonIndex].vocabs.push(vocabList[vocabIndex]);
}
}
}
}
// clean up - remove the comparison vocab from the homophone list - probably faster this way
for (var readingIndex = 0; readingIndex < homophones.length; readingIndex++){
for (var h = 0; h < homophones[readingIndex].vocabs.length; h++){
if(homophones[readingIndex].vocabs[h].character === comparisonVocab){
homophones[readingIndex].vocabs.splice(h,1);
h--;
}
}
// remove reading from homophones list if it does not contain any homophones
if(homophones[readingIndex].vocabs.length < 1){
homophones.splice(readingIndex,1);
readingIndex--;
}
}
}
// update data and class variables from cache
// returns true if available, else false
function loadDataFromLocal(){
var _lastUpdated, _data;
var obj = {};
_lastUpdated = getLastUpdated();
_data = GM_getValue(KEY_NAMES.DATA);
/* jshint eqnull:true */
if(_data == null || _lastUpdated == null){
return false;
} else {
lastUpdated = _lastUpdated; // global variable
userInformation = JSON.parse(_data).user_information; // hpx variable
vocabList = JSON.parse(_data).requested_information; // hpx variable
console.log("Loading data from cache");
return true;
}
/* jshint eqnull:false */
}
// get json using API; also saves it in GM_setValue
// param function(bool success, object data) callback, bool forceRefresh
// ** note getting all levels at once causes server errors - need to split up the request
function loadListFromServer(callback){
var LEVELS_PER_SET = 15;
var levels_per_set;
var level = 1;
var setIndex = 0;
var totalSetCount;
var dataSets = {};
var jsonData;
var jsonDataValid = true;
var responsesReceived = 0;
var callbackSent = false;
// speed things up the first time this program is run - just so the user knows what's up
if(typeof lastUpdated === 'undefined'){
levels_per_set = 5;
console.log('First time user detected. Please rest assured that after first run, HPX will no longer be making large quantites of API requests.');
} else {
levels_per_set = LEVELS_PER_SET;
}
totalSetCount = Math.ceil(MAX_LEVEL/levels_per_set);
while (level <= MAX_LEVEL){
var levels = '';
var urlBuild = API_REQUEST_TEMPLATE.VOCAB_LIST;
// build a levels string for the {levels} part of the request
// splitting each set into levels_per_set levels
for (var i = 0; i < levels_per_set && level <= MAX_LEVEL; level++, i++){
levels += level;
// add a ',' after every level except for the last one
if(level !== MAX_LEVEL && i + 1 !== levels_per_set){
levels += ',';
}
}
urlBuild = urlBuild.replace('{VERSION_NUMBER}',API_VERSION);
urlBuild = urlBuild.replace('{USER_API_KEY}',APIKey);
urlBuild = urlBuild.replace('{levels}',levels);
console.log(urlBuild);
(function(setID){
$.ajax({
method:'GET',
url:urlBuild,
dataType:'json'
}).done(function(data,status,xhr){
buildList(setID, true, data);
}).fail(function(xhr){
buildList(setID, false, xhr);
});
})(setIndex);
setIndex++;
}
// callback function from ajax requests - builds whole json file from multiple requests
// calls back when all requests have called back, in success or failure
// param bool success
function buildList(setID,success,data){
if(success){console.log('Data Set "'+setID+'" returned '+success);}
responsesReceived++;
if(success){
// check if the setID is already in dataSets and that the build has not already received a failure
if(!dataSets.hasOwnProperty(setID.toString()) && jsonDataValid){
dataSets[setID.toString()] = data;
} else {
// somehow got a duplicate record - failure!!
jsonDataValid = false;
jsonData = 'Duplicate record ' + setID.toString();
}
} else {
// fail response
jsonDataValid = false;
jsonData = data;
}
// check if all responses have been received
if (responsesReceived >= totalSetCount){
// consolidate dataSet into jsonData if no failure detected - data will be defined with xhr object or string if failed
if(jsonDataValid){
// copy first set **note that this process is not a true cloning process - copy by reference only
jsonData = dataSets['0'];
var setIndex = 1;
for (var i = 1;i < totalSetCount; i++){
// check that the parts come from the correct user
if(dataSets[i.toString()].user_information.username === jsonData.user_information.username){
// merge requested_information array
jsonData.requested_information = jsonData.requested_information.concat(dataSets[i.toString()].requested_information);
} else {
jsonData = 'User mismatch. Expected ' + jsonData.user_information.username + '. Got ' + dataSets[i.toString()].user_information.username;
jsonDataValid = false;
break;
}
}
}
// save if successful
if(jsonDataValid){
saveJson(jsonData);
}
// consolidated - now callback, whether it was successful or not
callback(jsonDataValid,jsonData);
}
}
function saveJson(jsonData){
console.log('Saving json');
GM_setValue (KEY_NAMES.DATA, JSON.stringify(jsonData));
GM_setValue (KEY_NAMES.LAST_UPDATED, new Date().getTime());
}
}
// split kana readings into arrays
function splitReadings(readings){
return readings.replace(/ /g,'').split(',');
}
}
// User interface controller
function UIPage(){
var elements = {}; // jQuery object DOM elements
var timeoutID;
var currentStatus;
var statuses = {
INIT:function(){
getComparisonVocab();
return 'Initiatizing';
},
IDLE:function(){
var time,days,hours,mins,secs;
var strTime = 'Last updated: ';
if(typeof lastUpdated === 'undefined' || lastUpdated < 1){
strTime += 'Never';
} else {
time = new Date().getTime() - lastUpdated;
days = Math.floor(time/(1000*60*60*24));
if(days > 0){
strTime+= days + ' day(s), ';
}
time %= 1000*60*60*24;
hours = Math.floor(time/(1000*60*60));
if(hours > 0){
strTime+= hours + ' hour(s), ';
}
time %= 1000*60*60;
mins = Math.floor(time/(1000*60));
if(mins > 0){
strTime+= mins + ' minute(s) and ';
}
time %= 1000*60;
secs = Math.floor(time/1000);
strTime+= secs + ' second(s) ago ';
}
return strTime;
},
SEARCHING_FOR_KEY: 'Looking for your API Key.',
SEARCH_FOR_KEY_FAILED:'Cannot find your API Key. Please try again later.',
FOUND_API_KEY:function(){
setTimeout(function(){
if(currentStatus === 'FOUND_API_KEY'){
this.setStatus('IDLE');
}
}.bind(this),3000);
return 'Retrieved API Key from your account page.';
},
CONNECTION_ERROR: function(){ // go back to idle after 2 seconds of displaying error message
setTimeout(function(){
if(currentStatus === 'CONNECTION_ERROR'){
this.setStatus('IDLE');
}
}.bind(this),2000);
return 'Connection error... Retrying momentarily';
},
UPDATING: 'Updating cache with Wanikani servers. Please stay on the page...', // API Servers
UPDATING_OTHER_INSTANCE: 'Updating cache on another instance.'
};
// build UI layout hierarchy
// using Wanikani's .kotaba-table-list to display the vocab
elements.hpxSection = $('<section>',{id:'hpx-ui','class':'kotoba-table-list'});
$('.vocabulary-reading').after(elements.hpxSection);
elements.infoResetHolder = $('<div>');
// section title/heading
elements.heading = $('<h2>',{text:'Homophones'});
// info h4
elements.info = $('<h4>',{'class':'small-caps',text:''});
elements.info.css('display','inline-block');
// reset button
elements.reset = $('<a>',{'class':'btn btn-mini hpx-btn'})
.css('margin-left','5px')
.css('float','right')
.text('Update cache now');
elements.reset.on('click',function(){
$(document).trigger('HPX:reloadRequest');
});
// reset API button
elements.resetAPI = $('<a>',{'class':'btn btn-mini hpx-btn'})
.css('margin-left','5px')
.css('float','right')
.text('Reload API Key');
elements.resetAPI.on('click',function(){
$(document).trigger('HPX:reloadAPIRequest');
});
// ul
elements.ul = $('<ul>',{'class':'multi-character-grid'});
// display p when no homophones found
elements.noHomophones = $('<p>',{text:'No homophones founds'});
elements.infoResetHolder
.append(elements.info)
.append(elements.reset)
.append(elements.resetAPI);
elements.hpxSection
.append(elements.heading)
.append(elements.infoResetHolder)
.append(elements.ul);
// build the list layout
this.displayHomophones = function(homophones){
var t = {};
// empty ul wrapper from previous renders
elements.ul.empty();
// add "no homophones found" if there were no homophones found
if (homophones.length < 1){
elements.ul.append(elements.noHomophones);
}
for (var readingsIndex = 0; readingsIndex < homophones.length; readingsIndex++){
for (var i = 0; i < homophones[readingsIndex].vocabs.length; i++){
// time to create list item for each item
t.liWrapper = $('<li>',{'class':'character-item', id:'vocabulary-' + homophones[readingsIndex].vocabs[i].character});
// set classes
if(homophones[readingsIndex].vocabs[i].user_specific === null){
// locked
t.liWrapper.addClass('locked');
} else if (homophones[readingsIndex].vocabs[i].user_specific.srs === 'burned') {
// burned
t.liWrapper.addClass('burned');
}
t.spanItemBadge = $('<span>',{'class':'item-badge', lang:'ja'});
t.anchor = $('<a>',{href:'/vocabulary/'+encodeURIComponent(homophones[readingsIndex].vocabs[i].character)});
t.spanCharacter = $('<span>',{'class':'character', lang:'ja', text:homophones[readingsIndex].vocabs[i].character});
t.ulWrapper = $('<ul>');
t.liReading = $('<li>',{lang:'ja', text:homophones[readingsIndex].vocabs[i].kana});
t.liMeaning = $('<li>',{text:homophones[readingsIndex].vocabs[i].meaning});
// append these elements appropriately
t.liWrapper.append(t.spanItemBadge)
.append(t.anchor);
t.anchor.append(t.spanCharacter)
.append(t.ulWrapper);
t.ulWrapper.append(t.liReading)
.append(t.liMeaning);
elements.ul.append(t.liWrapper);
}
// add a separator
if (readingsIndex < homophones.length - 1){
elements.ul.append($('<hr>'));
}
}
};
this.toggleResetButton = function(state){
if (typeof state === 'boolean'){
if(state){
elements.reset.removeAttr('disabled');
}
else {
elements.reset.attr('disabled','disabled');
}
}
};
// public function that allows the view to be set
// calls updateView() which updates the status text
this.setStatus = function(state){
console.log(state);
if(statuses.hasOwnProperty(state)){
currentStatus = state;
updateView.call(this);
}
function updateView(){
var res;
if(typeof statuses[currentStatus] === 'function'){
res = statuses[currentStatus].call(this);
} else {
res = statuses[currentStatus];
}
if(typeof timeoutID != 'undefined'){
clearTimeout(timeoutID);
elements.info.text(res);
}
timeoutID = setTimeout(function(){updateView(state);}.bind(this),1000);
}
};
// get the vocab of the current page from url
function getComparisonVocab(){
var comparisonVocab,
currentUrl = $(location).attr('href');
// create jQuery object with <a> DOM
var a = $('<a>',{href:currentUrl})[0];
// extract pathname from the url
var pathname = a.pathname;
// at this stage, pathname could be "/vocabulary/{vocab}" or "/vocabulary/{vocab}/" or "/level/[0-9]+/vocabulary/{vocab}" or "/level/[0-9]+/vocabulary/{vocab}/"
// remove "/vocabulary/" first then any trailing"/"
pathname = pathname.replace(/^.*\/vocabulary\//i,'');
pathname = pathname.replace(/\//,'');
// decode
comparisonVocab = decodeURIComponent(pathname);
console.log('Comparison vocab detected as ' + comparisonVocab);
// trigger new HPX:vocabUpdate event
$(document).trigger('HPX:vocabUpdate',{
exists: true,
comparisonVocab: comparisonVocab
});
}
}
// for /lesson/session and /review/session
function UISessionPage(){
var elements = {}; // jQuery object DOM elements for /review and /lesson
var lessonPage = ($(location).attr('href').search(/^http(s)?:\/\/www\.wanikani\.com\/lesson\/session/) === 0) ? true : false;
var currentStatus;
var statuses = {
INIT:function(){
return 'Initiatizing';
},
IDLE:function(){
return '';
},
FOUND_API_KEY:function(){
setTimeout(function(){
if(currentStatus === 'FOUND_API_KEY'){
this.setStatus('IDLE');
}
}.bind(this),3000);
return 'Retrieved API Key from your account page.';
},
SEARCHING_FOR_KEY: 'Looking for your API Key.',
SEARCH_FOR_KEY_FAILED:'Cannot find your API Key. Please try again later.',
CONNECTION_ERROR: function(){
setTimeout(function(){
if(currentStatus === 'CONNECTION_ERROR'){
this.setStatus('IDLE');
}
}.bind(this),2000);
return 'Connection error... Retrying momentarily';
},
UPDATING: 'Updating cache with Wanikani servers. Please stay on the page...',
UPDATING_OTHER_INSTANCE: 'Updating cache on another instance.'
};
elements.hpxSection = $('<section>',{id:'hpx-ui'})
.css('margin-top','21px'); // wrapper
elements.heading = $('<h2>',{text:'Homophones'}); // section title/heading
elements.ul = $('<ul>',{'class':'lattice-multi-character'}) // ul
.css('padding-left','0');
elements.noHomophones = $('<p>',{text:'No homophones founds'});// display p when no homophones found
elements.hpxSection
.append(elements.heading)
.append(elements.ul);
// hook jQuery.fn.show() - for review sections of /lesson and /review
$.fn._hpx_show = $.fn.show;
$.fn.show = function(a,b,c){
var res = $.fn._hpx_show.call(this,a,b,c);
// detect when Wanikani has loaded additional item information
// ("#all-info").show() seems to correspond with this.
if(typeof this[0] !== 'undefined' && this[0].id === 'information'){
// start of wanikani ajax request
} else if(typeof this[0] !== 'undefined' && this[0].id === 'all-info'){
// wanikani ajax request returns - does not fire with radicals
if(lessonPage){
getComparisonVocab($.jStorage.get('l/currentQuizItem'));
} else {
getComparisonVocab($.jStorage.get('currentItem'));
}
} else if (typeof this[0] === 'undefined'){
console.log(this); // i'm curious
}
return res;
};
if(lessonPage){
// hook on to $.jStorage.get function - for lesson the section of /lesson
$.jStorage._hpx_get = $.jStorage.get;
$.jStorage.get = function( key , defaultValue){
var res = $.jStorage._hpx_get( key , defaultValue);
if(key === 'l/currentLesson'){
if(res.voc){
getComparisonVocab(res);
}
}
return res;
};
}
// build the list layout
this.displayHomophones = function(homophones){
var t = {};
// empty ul wrapper from previous renders
elements.ul.empty();
// add "no homophones found" if there were no homophones found
if (homophones.length < 1){
elements.ul.append(elements.noHomophones);
}
for (var readingsIndex = 0; readingsIndex < homophones.length; readingsIndex++){
for (var i = 0; i < homophones[readingsIndex].vocabs.length; i++){
t.liWrapper = $('<li>',{ id:'vocabulary-' + homophones[readingsIndex].vocabs[i].character});
t.anchor = $('<a>',{
lang:'ja',
href:'/vocabulary/'+encodeURIComponent(homophones[readingsIndex].vocabs[i].character),
text:homophones[readingsIndex].vocabs[i].character
});
// append these elements appropriately
t.liWrapper.append(t.spanItemBadge)
.append(t.anchor);
elements.ul.append(t.liWrapper);
}
// add a separator
if (readingsIndex < homophones.length - 1){
elements.ul.append($('<hr>'));
}
}
// don't know if it's a review/quiz or lesson? why not both
if(lessonPage){
$("#supplement-voc-reading div.col1") // lesson
.append(elements.hpxSection);
}
$('#item-info-reading') // quiz/review
.append(elements.hpxSection);
};
this.toggleResetButton = function(state){};
// public function that allows the view to be set
// calls updateView() which updates the status text
this.setStatus = function(state){
console.log(state);
if(statuses.hasOwnProperty(state)){
currentStatus = state;
if(typeof statuses[currentStatus] === 'function'){
statuses[currentStatus].call(this);
}
}
};
// get the vocab of the current page from url
// wkObj is a vocab item plain object type. see wanikani.com/api for more info
function getComparisonVocab(wkObj){
var comparisonVocab = wkObj.voc ? wkObj.voc : undefined; // only for vocab
console.log('Comparison vocab detected as ' + comparisonVocab);
if(comparisonVocab){
// trigger new HPX:vocabUpdate event
$(document).trigger('HPX:vocabUpdate',{
exists: true,
comparisonVocab: comparisonVocab
});
}
}
// some styles
$('head')
.append($('<style>',{type:'text/css'})
.html('.lattice-multi-character a {display: block;color:#fff; text-shadow: 0 1px 0 rgba(0,0,0,0.2); text-decoration: none; '+
'box-shadow: 0 -2px 0 rgba(0,0,0,0.2) inset; transition: text-shadow ease-out 0.3s; padding-left: 0.4em; '+
'padding-right: 0.4em; font-size: 13px; border-radius: 3px; background-color: #3f7fe9;}'+
'.lattice-multi-character li{overflow-x:hidden; overflow-y:hidden; color: rgb(51, 51, 51); display:inline-block; '+
'width: auto;height: 21px;margin-right: 2px;margin-bottom: 2px;line-height: 21px;text-align: center;}'+
'.lattice-multi-character ul{margin-left: 0;margin-right: 0;}'));
}
// Attempts to find API Key in GM_getValue, localStorage and wanikani.com in that order
// bool forceRemote - flag to skip local search
// function(key) callback - callback function after ajax function calls back
// str key on success, else jqXhr object on failure
function findAPIKey(callback,forceRemote){
var key;
var keyRegex = /^[0-9a-f]{32}$/i;
ui.setStatus('SEARCHING_FOR_KEY');
// default value of forceRemote is false
if (!(typeof forceRemote !== 'undefined' && forceRemote)){
// Look for the key in userscript DB
key = GM_getValue(KEY_NAMES.API_KEY);
if(typeof key == 'undefined'){
console.log('Cannot find key from GM_getValue');
// Not in userscript DB
// Look in localstorage - helpful if API Key is defined by other scripts
var validKeyFound = false;
// v0.9.1 and up: removed localStorage search for apiKey as it is unreliable
/*
for (var i = 0; i < commonAPIKeyNames.length; i++){
key = localStorage.getItem(commonAPIKeyNames[i]);
// Check if key exists and fits regex
if(isValidKey(key)){
//Key from localStorage valid
validKeyFound = true;
break;
} // Else keep looping
}
*/
if (validKeyFound){
console.log('Found key in localStorage: ' + key);
saveKey(key);
ui.setStatus('IDLE');
return callback(key);
}
} else {
console.log('Found key in GM_getValue: ' + key);
ui.setStatus('IDLE');
return callback(key);
}
}
// find key on Wanikani settings page by way of AJAX
$.ajax({
method: 'GET',
url: SETTINGS_URL,
dataType: 'html'
}).done(function(data,status,xhr){
// Received successful response from server
// Parse responseText as HTML then create jQuery object
var page = $($.parseHTML(data));
// find key inside input element with id user_api_key
key = page.find('#user_api_key').val();
if(isValidKey(key)){
console.log('Found it from AJAX: ' + key);
saveKey(key);
ui.setStatus('FOUND_API_KEY');
callback(key);
} else {
console.log('Key not found in WaniKani account settings page. Please report to developer.');
ui.setStatus('SEARCH_FOR_KEY_FAILED');
}
}).fail( function(xhr){
// Did not receive successful response
console.log(xhr);
ui.setStatus('SEARCH_FOR_KEY_FAILED');
callback(xhr);
});
function saveKey(validKey){
GM_setValue(KEY_NAMES.API_KEY,validKey);
console.log('Key saved in GM_setValue');
}
function isValidKey(tryKey){
// key would be null if not set in localStorage
return (typeof tryKey !== 'undefined' && tryKey !== null && tryKey.search(keyRegex) != -1);
}
}
// for testing only
function GM_clearValues(){
var keys = GM_listValues();
for (var i = 0; i < keys.length; i++){
GM_deleteValue(keys[i]);
console.log('Deleted ' + keys[i]);
}
}
// http://www.html5rocks.com/en/tutorials/pagevisibility/intro/
function pageIsHidden(){
var prefixes = ['webkit','moz','ms','o'];
var property;
// if 'hidden' is natively supported just return it
if ('hidden' in document){
property = 'hidden';
} else {
// otherwise loop over all the known prefixes until we find one
for (var i = 0; i < prefixes.length; i++){
if ((prefixes[i] + 'Hidden') in document){
property = prefixes[i] + 'Hidden';
}
}
}
// otherwise hidden is not supported
return (typeof document[property] !== 'undefined' ? document[property] : false);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment