// ==UserScript==
// @name WaniKani Item Marker
// @description Tool to mark and track individual radicals/kanji/vocabulary
// @namespace irx.wanikani.marker
// @include https://www.wanikani.com/*
// @version 3.0
// @copyright 2016, Ingo Radax
// @license MIT; http://opensource.org/licenses/MIT
// @grant none
// @run-at document-start
// ==/UserScript==
var WaniKani = (function() {
var local_storage_prefix = 'wk_toolkit_';
var api_key = null;
var radical_data = null;
var kanji_data = null;
var vocabulary_data = null;
var radicals_with_image = [];
function log(msg) {
console.log(msg);
}
function is_on_wanikani() {
return (window.location.host == 'www.wanikani.com');
}
function is_on_dashboard() {
return is_on_wanikani() && ((window.location.pathname == '/dashboard') || (window.location.pathname == '/'));
}
function is_on_review_session_page() {
return is_on_wanikani() && (window.location.pathname == '/review/session');
}
function is_on_review_page() {
return is_on_wanikani() && (window.location.pathname == '/review');
}
function is_on_lesson_session_page() {
return is_on_wanikani() && (window.location.pathname == '/lesson/session');
}
function is_on_lesson_page() {
return is_on_wanikani() && (window.location.pathname == '/lesson');
}
function is_on_lattice_radicals_meaning() {
return is_on_wanikani() && (window.location.pathname == '/lattice/radicals/meaning');
}
function is_on_lattice_radicals_progress() {
return is_on_wanikani() && (window.location.pathname == '/lattice/radicals/progress');
}
function is_on_lattice_kanji_combined() {
return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/combined');
}
function is_on_lattice_kanji_meaning() {
return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/meaning');
}
function is_on_lattice_kanji_reading() {
return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/reading');
}
function is_on_lattice_kanji_progress() {
return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/status');
}
function is_on_lattice_vocabulary_combined() {
return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/combined');
}
function is_on_lattice_vocabulary_meaning() {
return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/meaning');
}
function is_on_lattice_vocabulary_reading() {
return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/reading');
}
function is_on_lattice_vocabulary_progress() {
return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/status');
}
//-------------------------------------------------------------------
// Try to parse the url and detect if it belongs to a single item.
// e.g. 'https://www.wanikani.com/level/1/radicals/construction'
// will be parsed as 'radicals' and 'construction'
//-------------------------------------------------------------------
function parse_item_url(url) {
if ((url == null) || (url == undefined)) {
return null;
}
if (url.indexOf('/lattice/') > -1) {
return null;
}
url = decodeURI(url);
var parsed = /.*\/(radicals|kanji|vocabulary)\/(.+)/.exec(url);
if (parsed) {
return {type:parsed[1], name:parsed[2]};
}
else {
return null;
}
}
function reset_radical_data(resetLocalStorage) {
radical_data = null;
if (resetLocalStorage) {
localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
}
}
function reset_kanji_data(resetLocalStorage) {
kanji_data = null;
if (resetLocalStorage) {
localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
}
}
function reset_vocabulary_data(resetLocalStorage) {
vocabulary_data = null;
if (resetLocalStorage) {
localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
}
}
function clear_local_storage() {
localStorage.removeItem(local_storage_prefix + 'last_review_time');
localStorage.removeItem(local_storage_prefix + 'next_review_time');
localStorage.removeItem(local_storage_prefix + 'last_unlock_time');
localStorage.removeItem(local_storage_prefix + 'api_key');
localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
}
function track_times() {
if (is_on_review_session_page()) {
localStorage.setItem(local_storage_prefix + 'last_review_time', now());
var lastUnlockTime = new Date($('.recent-unlocks time:nth(0)').attr('datetime'))/1000;
localStorage.setItem(local_storage_prefix + 'last_unlock_time', now());
}
if (is_on_dashboard()) {
var next_review = Number($('.review-status .timeago').attr('datetime'));
// Workaround for "WaniKani Real Times" script, which deletes the element we were looking for above.
if (isNaN(next_review)) {
next_review = Number($('.review-status time1').attr('datetime'));
// Conditional divide-by-1000, in case someone fixed this error in Real Times script.
if (next_review > 10000000000) next_review /= 1000;
}
localStorage.setItem(local_storage_prefix + 'next_review_time', next_review);
}
}
function get_last_review_time() {
return Number(localStorage.getItem(local_storage_prefix + 'last_review_time') || 0);
}
function get_next_review_time() {
return Number(localStorage.getItem(local_storage_prefix + 'next_review_time') || 0);
}
function get_last_unlock_time() {
return Number(localStorage.getItem(local_storage_prefix + 'last_unlock_time') || 0);
}
function now() {
return Math.floor(new Date() / 1000);
}
function ajax_retry(url, retries, timeout) {
retries = retries || 2;
timeout = timeout || 3000;
function action(resolve, reject) {
$.ajax({
url: url,
timeout: timeout
})
.done(function(data, status){
if (status === 'success')
resolve(data);
else
reject();
})
.fail(function(xhr, status, error){
if (status === 'error' && --retries > 0)
action(resolve, reject);
else
reject();
});
}
return new Promise(action);
}
function query_page_radicals_with_images() {
return new Promise(function(resolve, reject) {
ajax_retry('/lattice/radicals/meaning').then(function(page) {
if (typeof page !== 'string') {return reject();}
page = $(page);
radicals_with_image = [];
page.find('li').each(function(i, item) {
var attr = $(this).attr('id');
if (attr && attr.startsWith('radical-')) {
var radicalChar = $(this).text();
var href = $(this).find('a').attr('href');
var parsedHref = WaniKani.parse_item_url(href);
var image = $(this).find('img');
var imageSrc = '';
if (image.length > 0) {
imageSrc = image.attr('src');
}
radicals_with_image.push({ name: parsedHref.name, character: radicalChar, image: imageSrc });
}
});
resolve();
},function(result) {
reject(new Error('Failed to query page!'));
});
});
}
function get_radicals_with_image() {
return radicals_with_image;
}
function get_api_key() {
return new Promise(function(resolve, reject) {
api_key = localStorage.getItem(local_storage_prefix + 'api_key');
if (typeof api_key === 'string' && api_key.length == 32) {
log("Already having API key");
return resolve();
}
log("Loading API key");
ajax_retry('/account').then(function(page) {
log("Loading API key ... SUCCESS");
// --[ SUCCESS ]----------------------
// Make sure what we got is a web page.
if (typeof page !== 'string') {return reject();}
// Extract the user name.
page = $(page);
// Extract the API key.
api_key = page.find('#api-button').parent().find('input').attr('value');
if (typeof api_key !== 'string' || api_key.length !== 32) {return reject();}
localStorage.setItem(local_storage_prefix + 'api_key', api_key);
resolve();
},function(result) {
log("Loading API key ... ERROR");
// --[ FAIL ]-------------------------
reject(new Error('Failed to fetch API key!'));
});
});
}
function call_api_user_radicals() {
return new Promise(function(resolve, reject) {
log("Calling API: User radicals");
$.getJSON('/api/user/' + api_key + '/radicals/', function(json){
if (json.error && json.error.code === 'user_not_found') {
log("Calling API: User radicals ... ERROR")
reset_radical_data(true);
location.reload();
reject();
return;
}
log("Calling API: User radicals ... SUCCESS");
localStorage.setItem(local_storage_prefix + 'api_user_radicals', JSON.stringify(json));
localStorage.setItem(local_storage_prefix + 'api_user_radicals_fetch_time', now());
radical_data = json;
resolve();
});
});
}
function call_api_user_kanji() {
return new Promise(function(resolve, reject) {
log("Calling API: User kanji");
$.getJSON('/api/user/' + api_key + '/kanji/', function(json){
if (json.error && json.error.code === 'user_not_found') {
log("Calling API: User kanji ... ERROR")
reset_kanji_data(true);
location.reload();
reject();
return;
}
log("Calling API: User kanji ... SUCCESS");
localStorage.setItem(local_storage_prefix + 'api_user_kanji', JSON.stringify(json));
localStorage.setItem(local_storage_prefix + 'api_user_kanji_fetch_time', now());
kanji_data = json;
resolve();
});
});
}
function call_api_user_vocabulary() {
return new Promise(function(resolve, reject) {
log("Calling API: User vocabulary");
$.getJSON('/api/user/' + api_key + '/vocabulary/', function(json){
if (json.error && json.error.code === 'user_not_found') {
log("Calling API: User vocabulary ... ERROR")
reset_vocabulary_data(true);
location.reload();
reject();
return;
}
log("Calling API: User vocabulary ... SUCCESS");
localStorage.setItem(local_storage_prefix + 'api_user_vocabulary', JSON.stringify(json));
localStorage.setItem(local_storage_prefix + 'api_user_vocabulary_fetch_time', now());
vocabulary_data = json;
resolve();
});
});
}
function get_last_fetch_time_api_user_radicals() {
return Number(localStorage.getItem(local_storage_prefix + 'api_user_radicals_fetch_time'));
}
function get_last_fetch_time_api_user_kanji() {
return Number(localStorage.getItem(local_storage_prefix + 'api_user_kanji_fetch_time'));
}
function get_last_fetch_time_api_user_vocabulary() {
return Number(localStorage.getItem(local_storage_prefix + 'api_user_vocabulary_fetch_time'));
}
function load_radical_data() {
var next_review_time = get_next_review_time();
var last_review_time = get_last_review_time();
var last_unlock_time = get_last_unlock_time();
var last_fetch_time = get_last_fetch_time_api_user_radicals();
if ((last_fetch_time <= last_unlock_time) ||
(last_fetch_time <= last_review_time) ||
((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
log("Clearing previous fetched radical data");
radical_data = null;
localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
}
if (radical_data == null) {
var stringified = localStorage.getItem(local_storage_prefix + 'api_user_radicals');
if (stringified != null) {
log("Radical data loaded from local storage");
radical_data = JSON.parse(stringified);
}
}
if (radical_data != null) {
log("Radical data already loaded");
return Promise.resolve();
}
return new Promise(function(resolve, reject) {
get_api_key()
.then(call_api_user_radicals)
.then(function() {
resolve();
});
});
}
function load_kanji_data() {
var next_review_time = get_next_review_time();
var last_review_time = get_last_review_time();
var last_unlock_time = get_last_unlock_time();
var last_fetch_time = get_last_fetch_time_api_user_kanji();
if ((last_fetch_time <= last_unlock_time) ||
(last_fetch_time <= last_review_time) ||
((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
log("Clearing previous fetched kanji data");
kanji_data = null;
localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
}
if (kanji_data == null) {
var stringified = localStorage.getItem(local_storage_prefix + 'api_user_kanji');
if (stringified != null) {
log("Kanji data loaded from local storage");
kanji_data = JSON.parse(stringified);
}
}
if (kanji_data != null) {
log("Kanji data already loaded");
return Promise.resolve();
}
return new Promise(function(resolve, reject) {
get_api_key()
.then(call_api_user_kanji)
.then(function() {
resolve();
});
});
}
function load_vocabulary_data() {
var next_review_time = get_next_review_time();
var last_review_time = get_last_review_time();
var last_unlock_time = get_last_unlock_time();
var last_fetch_time = get_last_fetch_time_api_user_vocabulary();
if ((last_fetch_time <= last_unlock_time) ||
(last_fetch_time <= last_review_time) ||
((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
log("Clearing previous fetched vocabulary data");
vocabulary_data = null;
localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
}
if (vocabulary_data == null) {
var stringified = localStorage.getItem(local_storage_prefix + 'api_user_vocabulary');
if (stringified != null) {
log("Vocabulary data loaded from local storage");
vocabulary_data = JSON.parse(stringified);
}
}
if (vocabulary_data != null) {
log("Vocabulary data already loaded");
return Promise.resolve();
}
return new Promise(function(resolve, reject) {
get_api_key()
.then(call_api_user_vocabulary)
.then(function() {
resolve();
});
});
}
function get_radical_data() {
return radical_data;
}
function get_kanji_data() {
return kanji_data;
}
function get_vocabulary_data() {
return vocabulary_data;
}
function find_radical(meaning) {
if (radical_data == null) {
return null;
}
var numRadicals = radical_data.requested_information.length;
for (var i = 0; i < numRadicals; i++) {
if (radical_data.requested_information[i].meaning == meaning) {
return radical_data.requested_information[i];
}
}
return null;
}
function find_kanji(character) {
if (kanji_data == null) {
return null;
}
var numKanji = kanji_data.requested_information.length;
for (var i = 0; i < numKanji; i++) {
if (kanji_data.requested_information[i].character == character) {
return kanji_data.requested_information[i];
}
}
return null;
}
function find_vocabulary(character) {
if (vocabulary_data == null) {
return null;
}
var numVocabulary = vocabulary_data.requested_information.general.length;
for (var i = 0; i < numVocabulary; i++) {
if (vocabulary_data.requested_information.general[i].character == character) {
return vocabulary_data.requested_information.general[i];
}
}
return null;
}
function find_item(type, name) {
if (type == 'radicals') {
return find_radical(name);
}
else if(type == 'kanji') {
return find_kanji(name);
}
else if(type == 'vocabulary') {
return find_vocabulary(name);
}
else {
return null;
}
}
return {
is_on_wanikani: is_on_wanikani,
is_on_dashboard: is_on_dashboard,
is_on_review_session_page: is_on_review_session_page,
is_on_review_page: is_on_review_page,
is_on_lesson_session_page: is_on_lesson_session_page,
is_on_lesson_page: is_on_lesson_page,
is_on_lattice_radicals_meaning: is_on_lattice_radicals_meaning,
is_on_lattice_radicals_progress: is_on_lattice_radicals_progress,
is_on_lattice_kanji_combined: is_on_lattice_kanji_combined,
is_on_lattice_kanji_meaning: is_on_lattice_kanji_meaning,
is_on_lattice_kanji_reading: is_on_lattice_kanji_reading,
is_on_lattice_kanji_progress: is_on_lattice_kanji_progress,
is_on_lattice_vocabulary_combined: is_on_lattice_vocabulary_combined,
is_on_lattice_vocabulary_meaning: is_on_lattice_vocabulary_meaning,
is_on_lattice_vocabulary_reading: is_on_lattice_vocabulary_reading,
is_on_lattice_vocabulary_progress: is_on_lattice_vocabulary_progress,
query_page_radicals_with_images: query_page_radicals_with_images,
get_radicals_with_image: get_radicals_with_image,
parse_item_url: parse_item_url,
reset_radical_data: reset_radical_data,
reset_kanji_data: reset_kanji_data,
reset_vocabulary_data: reset_vocabulary_data,
clear_local_storage: clear_local_storage,
track_times: track_times,
get_last_review_time: get_last_review_time,
get_next_review_time: get_next_review_time,
load_radical_data: load_radical_data,
get_radical_data: get_radical_data,
find_radical: find_radical,
load_kanji_data: load_kanji_data,
get_kanji_data: get_kanji_data,
find_kanji: find_kanji,
load_vocabulary_data: load_vocabulary_data,
get_vocabulary_data: get_vocabulary_data,
find_vocabulary: find_vocabulary,
find_item: find_item,
};
})();
var UI = (function() {
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;
}
function initCss() {
var css =
'#item_marker {' +
' display:none;' +
'}' +
'#item_marker h3 {' +
' height: 24px;' +
' margin-top: 10px;' +
' margin-bottom: 0px;' +
' padding: 5px 20px;' +
' border-radius: 5px 5px 0 0;' +
' background-color: seagreen;' +
' color: white;' +
' text-shadow: none;' +
'}' +
'#item_marker section {' +
' background-color: lightgrey;' +
'}' +
'#item_marker table {' +
'}' +
'#item_marker td {' +
' padding: 2px 8px;' +
'}' +
'#item_marker .close_button {' +
' float: right;' +
' height: 24px;' +
'}'
;
addStyle(css);
}
function buildIdAttr(id) {
if (id && id != '')
return 'id="' + id + '"';
return '';
}
function addOnChangeListener(id, listener) {
$('#' + id).off('change');
$('#' + id).on('change', listener);
}
function addOnClickListener(id, listener) {
$('#' + id).off('click');
$('#' + id).on('click', listener);
}
function buildWindow(id, title) {
var html =
'<div ' + buildIdAttr(id) + ' class="container">' +
'<div class="row">' +
'<div class="span12" >' +
'<section id="' + id + '_body">' +
'<h3>' +
title +
'<button class="close_button" " ' + buildIdAttr(id + '_close_btn') + '>Close</button>' +
'</h3>' +
'</section>' +
'</div>' +
'</div>' +
'</div>'
;
return html;
}
function buildTable(id) {
var html =
'<table ' + buildIdAttr(id) + '>' +
'<tbody ' + buildIdAttr(id + '_body') + '>' +
'</tbody>' +
'</table>';
return html;
}
function buildTableRow(id, column1, column2) {
var html =
'<tr' + buildIdAttr(id) + '>' +
'<td>' + column1 + '</td>' +
'<td>' + column2 + '</td>' +
'</tr>';
return html;
}
function buildSelection(id, tooltip) {
var html =
'<select ' + buildIdAttr(id) + ' class="input" name="' + id + '" title="' + tooltip + '" />';
return html;
}
function addSelectionOption(selectId, value, label, selected) {
$('#' + selectId).append(
'<option value="' + value + '" ' + (selected ? 'selected' : '') + '>' +
label +
'</option>');
}
function buildCheckBox(id, checked) {
var html =
'<input ' + buildIdAttr(id) + ' type="checkbox" ' + (checked ? 'checked' : '') + '>';
return html;
}
return {
initCss: initCss,
buildWindow: buildWindow,
buildTable: buildTable,
buildTableRow: buildTableRow,
buildCheckBox: buildCheckBox,
buildSelection: buildSelection,
addSelectionOption: addSelectionOption,
addOnChangeListener: addOnChangeListener,
addOnClickListener: addOnClickListener,
};
})();
(function(gobj) {
var data;
var api_key;
var settingsWindowAdded = false;
var dropDownMenuExtended = false;
var settings = {
markedItemsBorderColor: 'black',
unmarkedItemsBorderColor: 'no_border',
toggleMarksWithLinks: false,
showReviewWarning: false,
addDelayBeforeAnswerIsPossible: false,
};
var availableBorderColors = [
{ value: 'no_border', label: 'No border' },
{ value: 'black', label: 'Black' },
{ value: 'darkgrey', label: 'Dark Grey' },
{ value: 'lightgrey', label: 'Light Grey' },
{ value: 'white', label: 'White' },
{ value: 'red', label: 'Red' },
{ value: 'orange', label: 'Orange' },
{ value: 'yellow', label: 'Yellow' },
{ value: 'green', label: 'Green' },
{ value: 'blue', label: 'Blue' },
{ value: 'indigo', label: 'Indigo' },
{ value: 'violet', label: 'Violet' },
{ value: 'cyan', label: 'Cyan' }
];
var localStoragePrefix = 'ItemMarker_';
//-------------------------------------------------------------------
// Main function
//-------------------------------------------------------------------
function main() {
console.log('START - WaniKani Item Marker');
loadData();
loadSettings();
WaniKani.track_times();
extendDropDownMenu();
updatePage();
console.log('END - WaniKani Item Marker');
}
window.addEventListener('load', main, false);
//window.addEventListener('focus', main, false);
function extendDropDownMenu() {
if (dropDownMenuExtended) {
return;
}
$('<li><a href="#item_marker">Item Marker</a></li>')
.insertBefore($('.account .dropdown-menu .nav-header:eq(1)'))
.on('click', toggleSettingsWindow);
dropDownMenuExtended = true;
}
function buildSettingsWindow() {
UI.initCss();
var html;
html = UI.buildWindow('item_marker', 'Item Marker');
$(html).insertAfter($('.navbar'));
html = UI.buildTable('item_marker_settings');
console.log(html);
$('#item_marker_body').append(html);
html = UI.buildTableRow(
'',
'Border color for marked items',
UI.buildSelection(
'border_color_marked_items',
'Select the border color for marked items'));
$('#item_marker_settings_body').append(html);
for (var i = 0; i < availableBorderColors.length; i++) {
UI.addSelectionOption('border_color_marked_items',
availableBorderColors[i].value,
availableBorderColors[i].label,
availableBorderColors[i].value == settings.markedItemsBorderColor);
}
html = UI.buildTableRow(
'',
'Border color for unmarked items',
UI.buildSelection(
'border_color_unmarked_items',
'Select the border color for unmarked items'));
$('#item_marker_settings_body').append(html);
for (var i = 0; i < availableBorderColors.length; i++) {
UI.addSelectionOption('border_color_unmarked_items',
availableBorderColors[i].value,
availableBorderColors[i].label,
availableBorderColors[i].value == settings.unmarkedItemsBorderColor);
}
html = UI.buildTableRow(
'',
'Use links to mark/unmark items',
UI.buildCheckBox(
'use_links_to_toggle_items',
settings.toggleMarksWithLinks));
$('#item_marker_settings_body').append(html);
html = UI.buildTableRow(
'',
'Show warning during reviews',
UI.buildCheckBox(
'show_review_warning',
settings.showReviewWarning));
$('#item_marker_settings_body').append(html);
html = UI.buildTableRow(
'',
'Add 30 second delay before review answer is possible',
UI.buildCheckBox(
'add_delay_before_answer_is_possible',
settings.addDelayBeforeAnswerIsPossible));
$('#item_marker_settings_body').append(html);
UI.addOnChangeListener('border_color_marked_items',
function()
{
settings.markedItemsBorderColor = $('#border_color_marked_items').val();
saveSettings();
updateLinks();
});
UI.addOnChangeListener('border_color_unmarked_items',
function()
{
settings.unmarkedItemsBorderColor = $('#border_color_unmarked_items').val();
saveSettings();
updateLinks();
});
UI.addOnChangeListener('use_links_to_toggle_items',
function()
{
settings.toggleMarksWithLinks = $('#use_links_to_toggle_items').is(':checked');
saveSettings();
updateLinks();
});
UI.addOnChangeListener('show_review_warning',
function()
{
settings.showReviewWarning = $('#show_review_warning').is(':checked');
saveSettings();
updateLinks();
});
UI.addOnChangeListener('add_delay_before_answer_is_possible',
function()
{
settings.addDelayBeforeAnswerIsPossible = $('#add_delay_before_answer_is_possible').is(':checked');
saveSettings();
updateLinks();
});
UI.addOnClickListener('item_marker_close_btn',
function(e)
{
toggleSettingsWindow(e);
});
settingsWindowAdded = true;
}
function toggleSettingsWindow(e) {
if (e !== undefined) e.preventDefault();
// Add the manager if not already.
if (!settingsWindowAdded) buildSettingsWindow();
$('#item_marker').slideToggle();
$('html, body').animate({scrollTop: 0}, 800);
}
//-------------------------------------------------------------------
// Update the current page and add the item marker features
//-------------------------------------------------------------------
function updatePage() {
if (WaniKani.is_on_dashboard()) {
updateDashboardPage();
}
else if (WaniKani.is_on_review_page() || WaniKani.is_on_lesson_page()) {
updateReviewAndLessonPage();
}
else if (WaniKani.is_on_review_session_page()) {
updateReviewSessionPage();
}
else {
var location = decodeURI(window.location);
var parsedUrl = WaniKani.parse_item_url(location);
if (parsedUrl) {
updateItemPage(parsedUrl.type, parsedUrl.name);
}
}
updateLinks();
}
function updateLinks() {
$('a').each(function(i, item) {
var href = $(this).attr('href');
updateLinkFunction(href, $(this));
updateItemBorder(href, $(this));
});
$('ul.alt-character-list a').each(function(i, item) {
var href = $(this).attr('href');
var parsedUrl = WaniKani.parse_item_url(href);
if (parsedUrl) {
$(this).css('box-shadow', '');
updateItemBorder(href, $(this).parent('li'));
}
});
$('#marked_items_list a').each(function(i, item) {
var href = $(this).attr('href');
var parsedUrl = WaniKani.parse_item_url(href);
if (parsedUrl) {
$(this).css('box-shadow', '');
updateItemBorder(href, $(this).children('span:nth(0)'));
}
});
}
function updateLinkFunction(url, htmlElem) {
var parsedUrl = WaniKani.parse_item_url(url);
if (parsedUrl) {
if (settings.toggleMarksWithLinks) {
htmlElem.off('click');
htmlElem.on('click',
function(e)
{
e.preventDefault();
toggleItemByUri(url);
});
}
else {
htmlElem.off('click');
}
}
}
function updateItemBorder(url, htmlElem) {
var parsedUrl = WaniKani.parse_item_url(url);
if (parsedUrl) {
var item_index = indexOf(parsedUrl.type, parsedUrl.name);
if (item_index === -1) {
if (settings.unmarkedItemsBorderColor == 'no_border') {
htmlElem.css('box-shadow', '');
}
else {
htmlElem.css('box-shadow', '0 0 0 2px ' + settings.unmarkedItemsBorderColor + ' inset');
}
}
else {
if (settings.markedItemsBorderColor == 'no_border') {
htmlElem.css('box-shadow', '');
}
else {
htmlElem.css('box-shadow', '0 0 0 2px ' + settings.markedItemsBorderColor + ' inset');
}
}
}
}
//-------------------------------------------------------------------
// Load item marker data from local storage
//-------------------------------------------------------------------
function loadData() {
data = localStorage.getItem(localStoragePrefix + 'markedItems');
if (data == null) {
data = { items:[] };
}
else {
data = JSON.parse(data);
}
}
function loadSettings() {
var storedSettings = localStorage.getItem(localStoragePrefix + 'settings');
if (storedSettings) {
settings = JSON.parse(storedSettings);
}
}
//-------------------------------------------------------------------
// Save item marker data to local storage
//-------------------------------------------------------------------
function saveData() {
localStorage.setItem(localStoragePrefix + 'markedItems', JSON.stringify(data));
}
function saveSettings() {
localStorage.setItem(localStoragePrefix + 'settings', JSON.stringify(settings));
}
//-------------------------------------------------------------------
// Return the index of the given item in the list
// returns -1 if item isn't in list
//-------------------------------------------------------------------
function indexOf(type, name) {
return data.items.findIndex(function(item) { return (item.type == type) && (item.name == name); });
}
//-------------------------------------------------------------------
// Remove all marks
//-------------------------------------------------------------------
function unmarkAllItems() {
loadData();
data.items = [];
saveData();
updatePage();
}
//-------------------------------------------------------------------
// Force refresh of page and data
//-------------------------------------------------------------------
function forceRefresh() {
WaniKani.reset_radical_data(true);
for (var i = 0; i < data.items.length; i++) {
if (data.items[i].type == 'radicals') {
data.items[i].radical_character = null;
data.items[i].radical_image = null;
}
}
updatePage();
}
//-------------------------------------------------------------------
// Unmark a single item
//-------------------------------------------------------------------
function unmarkItem(type, name) {
loadData();
var index = indexOf(type, name);
if (index > -1) {
data.items.splice(index, 1);
saveData();
}
updatePage();
}
function toggleItemByUri(uri) {
var parsedUrl = WaniKani.parse_item_url(uri);
if (parsedUrl) {
var index = indexOf(parsedUrl.type, parsedUrl.name);
if (index > -1) {
unmarkItem(parsedUrl.type, parsedUrl.name);
}
else {
markItem(parsedUrl.type, parsedUrl.name);
}
}
}
//-------------------------------------------------------------------
// Sort the list of marked items
//-------------------------------------------------------------------
function sortItems() {
loadData();
data.items.sort(
function(a, b) {
if (a.type == 'radicals') {
if (b.type != 'radicals') {
return -1;
}
}
else if (a.type == 'kanji') {
if (b.type == 'radicals') {
return 1;
}
else if (b.type == 'vocabulary') {
return -1;
}
}
else {
if (b.type != 'vocabulary') {
return 1;
}
}
return a.name.localeCompare(b.name);
});
saveData();
updatePage();
}
//-------------------------------------------------------------------
// Mark a single item
//-------------------------------------------------------------------
function markItem(type, name) {
loadData();
var index = indexOf(type, name);
if (index === -1) {
data.items.push({type: type, name: name, radical_character: null, radical_image: null});
saveData();
}
updatePage();
}
//-------------------------------------------------------------------
// Callback if the currentItem changes
//-------------------------------------------------------------------
function onCurrentItemChanged() {
updatePage();
}
//-------------------------------------------------------------------
// Extends the review session page
// - Adds buttons to mark/unmark the current item
//-------------------------------------------------------------------
function updateReviewSessionPage() {
$.jStorage.stopListening('currentItem', onCurrentItemChanged);
$.jStorage.listenKeyChange('currentItem', onCurrentItemChanged);
var currentItem = $.jStorage.get('currentItem');
var name;
var type;
if (currentItem.hasOwnProperty('rad')) {
type = 'radicals';
name = currentItem.en[0].toLowerCase().replace(/\s/g, "-");
}
else if (currentItem.hasOwnProperty('kan')) {
type = 'kanji';
name = currentItem.kan;
}
else if (currentItem.hasOwnProperty('voc')) {
type = 'vocabulary';
name = currentItem.voc;
}
else {
return;
}
//console.log('updateReviewSessionPage - type: ' + type);
//console.log('updateReviewSessionPage - name: ' + name);
$('#option-mark').remove();
$('#option-unmark').remove();
$('#reviewWarning').remove();
$('#warning1').remove();
$('#warning2').remove();
var item_index = indexOf(type, name);
if (item_index === -1) {
$('#additional-content ul').append('<li id="option-mark"><span title="Mark this item">Mark</span></li>');
$('#option-mark').on('click', function() { markItem(type, name); });
$('#answer-form form').show();
$('#answer-form input').focus();
}
else {
$('#additional-content ul').append('<li id="option-unmark"><span title="Unmark this item">Unmark</span></li>');
$('#option-unmark').on('click', function() { unmarkItem(type, name); });
if (settings.showReviewWarning) {
$('#character').append('<span id="warning1"> !!!</span>');
$('#character').prepend('<span id="warning2">!!! </span>');
}
if (settings.addDelayBeforeAnswerIsPossible) {
$('#answer-form').prepend('<span id="reviewWarning"> <br />Beware, it\'s a marked item! Think for a moment about your answer.<br /> </span>');
$('#answer-form form').hide();
setTimeout(function updateReviewSessionPage() {
$('#answer-form form').show();
$('#answer-form input').focus();
$('#reviewWarning').remove();
}, 30000);
}
}
calculateDynamicWidthForReviewPage();
}
//-------------------------------------------------------------------
// Updates the dynamic with for the review page
//-------------------------------------------------------------------
function calculateDynamicWidthForReviewPage(){
var liCount = $('#additional-content ul').children().size();
var percentage = 100 / liCount;
percentage -= 0.1;
var cssDynamicWidth =
'#additional-content ul li {' +
' width: ' + percentage + '% !important' +
'} ';
addStyle(cssDynamicWidth);
}
//-------------------------------------------------------------------
// Adds a css to the page
//-------------------------------------------------------------------
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;
}
//-------------------------------------------------------------------
// Extends the review and lesson page
// - Adds a black/white border around marked/unmarked items displayed on the page
//-------------------------------------------------------------------
function updateReviewAndLessonPage() {
var query = $('div.active li');
if (query.length == 0) { // Page not completly loaded yet
setTimeout(updatePage, 1000);
return;
}
updateLinks();
}
//-------------------------------------------------------------------
// Extends the dashboard
// - Adds a 'marked items' section
// - Adds common buttons and marked items list to section
//-------------------------------------------------------------------
function updateDashboardPage() {
var query = $('section.progression');
if (query.length != 1) {
return;
}
$('#marked_items').remove();
$('section.progression').after('<section id="marked_items" />');
$('#marked_items').append('<h2>Marked items</h2>');
$('#marked_items').append('<p id="marked_items_buttons" />');
addCommonButtons();
addMarkedItemsList();
}
//-------------------------------------------------------------------
// Extends the an item page
// - Adds a 'marked items' section
// - Adds common buttons and marked items list to section
// - Adds buttons to mark/unmark the current item
//-------------------------------------------------------------------
function updateItemPage(type, name) {
var query = $('section#information');
if (query.length != 1) {
return;
}
$('#marked_items').remove();
$('section#information').after('<section id="marked_items" />');
$('#marked_items').append('<h2>Marked items</h2>');
$('#marked_items').append('<p id="marked_items_buttons" />');
var index = indexOf(type, name);
if (index === -1) {
var button = $('<button>Mark "' + name + '"</button>');
button.on('click', function() { markItem(type, name); });
$('#marked_items_buttons').append(button)
}
else {
var button = $('<button>Unmark "' + name + '"</button>');
button.on('click', function() { unmarkItem(type, name); });
$('#marked_items_buttons').append(button)
}
addCommonButtons();
addMarkedItemsList();
}
//-------------------------------------------------------------------
// Adds common buttons that are used on multiple locations
//-------------------------------------------------------------------
function addCommonButtons() {
if (data.items.length > 0)
{
var button = $('<button>Unmark all</button>');
button.on('click', function() { unmarkAllItems(); });
$('#marked_items_buttons').append(button)
}
if (data.items.length > 0)
{
var button = $('<button>Sort</button>');
button.on('click', function() { sortItems(); });
$('#marked_items_buttons').append(button)
}
{
var button = $('<button>Force refresh</button>');
button.on('click', function() { forceRefresh(); });
$('#marked_items_buttons').append(button)
}
}
function buildItemList() {
detect_radical_characters();
for (var i = 0; i < data.items.length; i++) {
var item = data.items[i];
var typeForClass = item.type;
if (typeForClass == 'radicals')
typeForClass = 'radical';
var itemText = item.name;
if (item.type == 'radicals') {
if (item.radical_image && item.radical_image != '') {
itemText = '<img src="' + item.radical_image + '"/>';
}
else if (item.radical_character != '') {
itemText = item.radical_character;
}
else {
itemText = '<i class="radical-' + item.name + '"></i>';
}
}
$('#marked_items_list').append(
'<a href="/' + item.type + '/' + item.name + '">' +
' <span class="' + typeForClass + '-icon" lang="ja">' +
' <span style="display: inline-block; margin-top: 0.1em;" class="japanese-font-styling-correction">' +
itemText +
' </span>' +
' </span>' +
'</a>');
}
updateLinks();
};
//-------------------------------------------------------------------
// Adds the list of marked items to the current page
//-------------------------------------------------------------------
function addMarkedItemsList() {
$('#marked_items').append('<span id="marked_items_list" />')
if (data.items.length == 0) {
$('#marked_items_list').append('<p>No marked items</p>');
}
else {
WaniKani.query_page_radicals_with_images().then(buildItemList);
}
}
//-------------------------------------------------------------------
// Uses the radical info to determine if a radical uses a character
// or an image.
//-------------------------------------------------------------------
function detect_radical_characters() {
var radicals_with_images = WaniKani.get_radicals_with_image();
$(data.items).each(function(i, item) {
if ((item.type == 'radicals') && (item.radical_character == null)) {
var radical_character = null;
var foundRadical = radicals_with_images.find(function(radical_with_image) {
return radical_with_image.name == item.name;
});
if (!foundRadical) {
return;
}
console.log('detect_radical_characters: ' + item.name + ' -> ' + foundRadical.character + ' / ' + foundRadical.image);
data.items[i].radical_character = foundRadical.character;
data.items[i].radical_image = foundRadical.image;
}
});
saveData();
}
}());