';
// Add the dialog to the DOM, and display it.
dialog = $(str);
$('#timeln').append(dialog);
dialog.css('top', t.top+25).css('left', t.left+10);
dialog.removeClass('hidden');
// Set up handler for the 'Save' and 'Cancel' buttons.
$('#timeln-settings button').on('click', function(e) {
dialog.addClass('hidden');
var max_days = Number(get_setting('max_days'));
var placement = get_setting('placement');
var save = e.target.value == 1;
if (save) {
set_setting('24hour', $('#timeln-settings .input[name="24hour"]').val()=="true");
set_setting('jp_font', $('#timeln-settings .input[name="jp_font"]').val());
set_setting('show_detail', $('#timeln-settings .input[name="show_detail"]').val()=="true");
set_setting('rescale_redraw', $('#timeln-settings .input[name="rescale_redraw"]').val()=="true");
set_setting('summary_bar_only', $('#timeln-settings .input[name="bar_style"]').val()=="true");
set_setting('show_current_bars', Number($('#timeln-settings .input[name="show_bars"]').val()) % 2 == 1);
set_setting('show_burn_bars', Number($('#timeln-settings .input[name="show_bars"]').val()) >= 2);
set_setting('mark_current', $('#timeln-settings .input[name="mark_current"]').val()!="0");
set_setting('mark_current_vocab', $('#timeln-settings .input[name="mark_current"]').val()=="2");
set_setting('graph_height', Number($('#timeln-settings .input[name="graph_height"]').val()));
$('#timeln-graph').height(Number(get_setting('graph_height')));
var new_max_days = Number($('#timeln-settings .input[name="max_days"]').val());
if (new_max_days != max_days) {
set_setting('max_days', new_max_days);
$('#range_input').attr('max', new_max_days);
}
var new_placement = $('#timeln-settings .input[name="placement"]').val();
if (new_placement != placement) {
set_setting('placement', new_placement);
place_timeline();
}
draw_timeline();
} else {
var jp_font = get_setting('jp_font');
$('#timeln-settings .input[name="24hour"]').val(get_setting('24hour').toString());
$('#timeln-settings .input[name="jp_font"]').val(jp_font);
$('#timeln-style').html('#timeln [lang="jp"] {font-family:'+jp_font+';}');
$('#timeln-settings .input[name="show_detail"]').val(get_setting('show_detail').toString());
$('#timeln-settings .input[name="rescale_redraw"]').val(get_setting('rescale_redraw').toString());
$('#timeln-settings .input[name="bar_style"]').val(get_setting('summary_bar_only').toString());
$('#timeln-settings .input[name="show_bars"]').val(
(get_setting('show_current_bars')===true ? 1 : 0) + (get_setting('show_burn_bars')===true ? 2 : 0)
);
$('#timeln-settings .input[name="mark_current"]').val(
(get_setting('mark_current').toString() ? (get_setting('mark_current_vocab').toString() ? '2' : '1') : '0')
);
$('#timeln-settings .input[name="graph_height"]').val(get_setting('graph_height').toString());
$('#timeln-settings .input[name="max_days"]').val(max_days.toFixed(2));
$('#timeln-settings .input[name="placement"]').val(placement);
}
});
// Set up handler to update the font when font selection changes.
$('#timeln-settings .input[name="jp_font"]').on('change', function(e) {
$('#timeln-style').html('#timeln [lang="jp"] {font-family:'+e.target.value+';}');
});
}
//-------------------------------------------------------------------
// Run when 'refresh' link is clicked.
//-------------------------------------------------------------------
function click_refresh() {
clear_cache();
$('#range_form, #graph-bar-info, #graph-item-info, #timeln-help, #timeln-settings').addClass('hidden');
$('#timeline').remove();
startup(true);
}
//-------------------------------------------------------------------
// Run when 'help' link is clicked.
//-------------------------------------------------------------------
function click_help() {
// Hide any other open windows.
$('#timeln-settings').addClass('hidden');
var t = $('#timeln').position();
var dialog = $('#timeln-help');
// If settings dialog already exists, show it and exit.
if (dialog.length > 0) {
if (!dialog.is(':visible'))
dialog.css('top', t.top+25).css('left', t.left+10);
dialog.toggleClass('hidden');
return;
}
// The dialog doesn't exist. Create it.
str =
'
'+
'
Timeline Help
'+
'
WaniKani Ultimate Timeline displays a schedule of your upcoming reviews.
'+
'
'+
' X-axis: Time when reviews become available. '+
' Y-axis: Number of reviews in a timeslot. '+
' Range slider: Set the number of hours to display on the graph.
'+
'
Hover over a graph bar to display a detail window, which shows details about the reviews in that timeslot.
'+
'
Click on a graph bar to anchor the detail window, then hover over individual review items for individual item info.
'+
'
Click and drag along the top of the X-axis to highlight a time range. The detail window will show details about all reviews in that time range.
'+
'
Current level reviews are indicated by a white arrow below the timeslot, a white background behind the timeslot, and a yellow "Current Level" box in the detail window. '+
' Burn reviews are indicated by a black arrow below the timeslot, a black background behind the timeslot, and a black "Burn Items" box in the detail window.
'+
'
'+
' Graph updates occur automatically every 15 minutes, and the timescale slowly moves to the left.'+
' As time passes, your available reviews will accumulate in the left-most timeslot, which represents "now".
'+
'
Forced refresh is like clearing your browser cache. It is usually only needed if you do reviews on a different device or computer.'+
' Normally, you only need to return to the WaniKani dashboard after doing reviews, and the timeline will fetch your updated schedule.
'+
'
'+
' Contact: Robin Findley (rfindley@usa.net)
'+
' '+
'
';
// Add the dialog to the DOM, and display it.
dialog = $(str);
$('#timeln').append(dialog);
dialog.css('top', t.top+25).css('left', t.left+10);
dialog.removeClass('hidden');
// Set up handler for the 'Ok' button.
$('#timeln-help button').on('click', function(e) {
dialog.addClass('hidden');
});
}
//-------------------------------------------------------------------
// Change the value of a setting.
//-------------------------------------------------------------------
function set_setting(name, value) {
settings[name] = value;
localStorage.setItem('timeln_settings', JSON.stringify(settings));
}
//-------------------------------------------------------------------
// Clear timeline data cache.
//-------------------------------------------------------------------
function clear_cache() {
// localStorage.removeItem('timeln_username');
localStorage.removeItem('apiKey');
localStorage.removeItem('timeln_cache');
localStorage.removeItem('timeln_last_fetch');
localStorage.removeItem('timeln_last_review');
}
//-------------------------------------------------------------------
// Retrieve the value of a setting.
//-------------------------------------------------------------------
function get_setting(name) {
return settings[name];
}
//-------------------------------------------------------------------
// Close the modal window.
//-------------------------------------------------------------------
function close_modal() {
$('#timeln-modal').remove();
}
//-------------------------------------------------------------------
// Set up a full-screen modal window at z-index-10 to catch events.
//-------------------------------------------------------------------
function open_modal(events, handler) {
var modal = $('');
modal.height($(document).height());
$('body').prepend(modal);
modal.on(events, handler);
}
//-------------------------------------------------------------------
// Event handler for item details.
//-------------------------------------------------------------------
function item_info_event(e) {
var hinfo = $('#graph-item-info');
var target = $(e.target);
switch (e.type) {
//-----------------------------
case 'mouseenter':
var item;
var type = target.data('type');
var str = '
';
hinfo.html(str);
hinfo.css('left', target.offset().left - target.position().left);
hinfo.css('top', Math.floor(target.offset().top + target.outerHeight() + 3));
hinfo.removeClass('hidden');
break;
//-----------------------------
case 'mouseleave':
hinfo.addClass('hidden');
break;
}
}
//-------------------------------------------------------------------
// Generate a formatted date string.
//-------------------------------------------------------------------
function format_date(time) {
var str;
if (time.getTime() === calc_time) return 'Now';
if (time.getDate() === (new Date()).getDate())
str = 'Today';
else
str = 'SunMonTueWedThuFriSat'.substr(time.getDay()*3, 3);
if (settings['24hour'])
str += ' ' + ('0' + time.getHours()).slice(-2) + ':' + '00153045'.substr(Math.floor(time.getMinutes()/15)*2, 2);
else
str += ' ' + ('0' + (((time.getHours()+11)%12)+1)).slice(-2) + ':' + '00153045'.substr(Math.floor(time.getMinutes()/15)*2, 2) + 'ap'[Math.floor(time.getHours()/12)] + 'm';
return str;
}
//-------------------------------------------------------------------
// Populate the info box.
//-------------------------------------------------------------------
function populate_info(slot_idx1, slot_idx2, hide_detail) {
// Check arguments, assign default values when missing.
if (slot_idx2 === undefined) slot_idx2 = slot_idx1+1;
if (hide_detail === undefined) hide_detail = false;
// Consolidate the selected range into a single structure of review items.
var si;
var si1 = Math.min(slot_idx1, slot_idx2);
var si2 = Math.max(slot_idx1, slot_idx2);
var hinfo = $('#graph-bar-info');
var slot_sum = {radicals:[], kanji:[], vocabulary:[], item_count:0, has_current:false, has_burn:false};
for (si = si1; si < si2; si++) {
var slot = timeline[si];
if (slot === undefined) continue;
slot_sum.radicals = slot_sum.radicals.concat(slot.radicals);
slot_sum.kanji = slot_sum.kanji.concat(slot.kanji);
slot_sum.vocabulary = slot_sum.vocabulary.concat(slot.vocabulary);
slot_sum.item_count += slot.item_count;
slot_sum.has_current |= slot.has_current;
slot_sum.has_burn |= slot.has_burn;
}
// If no items are in the range, hide the detail window.
if (slot_sum.item_count === 0) {
hinfo.addClass('hidden');
return;
}
// Save a global copy of the consolidated info for use in support functions.
graph_detail_items = slot_sum;
// Print the date or date range).
var str = format_date(new Date(calc_time + si1 * 900000));
if (si2-si1 > 1) str += ' to ' + format_date(new Date(calc_time + si2 * 900000));
// Populate item type summaries.
str += '
';
// If details are enabled, populate the review-item list.
var idx, item;
var show_detail = get_setting('show_detail') && !hide_detail;
if (show_detail) {
str += '
';
}
// We are done building the info box. Add it to the DOM.
hinfo.css('max-width', $('#timeln-graph').width()/2 - 15);
hinfo.html(str);
// Add event handlers for hovering review items.
if (show_detail) {
$('#timeln .detail').on('mouseenter', 'li', item_info_event);
$('#timeln .detail').on('mouseleave', item_info_event);
}
// If we are displaying a range, position the info box below the timeline.
// If the user is just hovering over a timeslot, the info box is positioned in a different function.
if (graph_hilight_mode != 0) {
var tlpos = $('#timeline')[0].getBoundingClientRect();
if (si1 <= graph_hours*2)
hinfo.css('left', tlpos.left + Math.floor(Math.min(graph_hilight_x1,graph_hilight_x2)));
else
hinfo.css('left', tlpos.left + Math.floor(Math.max(graph_hilight_x1,graph_hilight_x2)) - hinfo.outerWidth());
hinfo.css('top', tlpos.top + window.scrollY + graph_height_panel);
}
hinfo.removeClass('hidden');
}
//-------------------------------------------------------------------
// Event handler for time slots.
//-------------------------------------------------------------------
function bar_events(e) {
// Don't accept events while user is selecting a range.
if (detail_latched || graph_hilight_mode != 0) return;
var hinfo = $('#graph-bar-info');
var target = $(e.target);
var slot_idx = target.data('slot');
switch (e.type) {
//-----------------------------
case 'mousemove':
e.stopPropagation();
// We only want to redraw the info box just as we enter a new time slot.
if (slot_idx !== current_slot) {
current_slot = slot_idx;
// Populate the info box.
populate_info(slot_idx);
// Set the info box position, and unhide.
var left = e.target.getBoundingClientRect().left;
if (slot_idx < graph_hours*2)
hinfo.css('left', Math.floor(left + e.target.width.baseVal.value)+3);
else
hinfo.css('left', Math.floor(left - hinfo.outerWidth())-2);
}
// Update the vertical position even if we're on the
// same time slot, so box follows cursor vertically.
hinfo.css('top', e.pageY - 30);
break;
//-----------------------------
case 'mouseleave':
hinfo.addClass('hidden');
current_slot = undefined;
break;
//-----------------------------
case 'click':
detail_latched = true;
populate_info(slot_idx);
e.stopPropagation();
// Wait for a click anywhere on the document to close the info window.
$('body').on('click.close_tip', function(e) {
// Ignore clicks on the info box itself.
var tip = $('#graph-bar-info')[0];
if (e.target === tip || $.contains(tip, e.target)) return;
// Click was outside of info box. Close info box.
detail_latched = false;
current_slot = undefined;
hinfo.addClass('hidden');
$('body').off('.close_tip');
// If we clicked on another slot, make info sticky for that slot
// by simulating an additional click on that item.
if ($.contains($('#timeline .bars')[0], e.target)) {
var t = $(e.target);
t.trigger('mousemove');
t.trigger('click');
}
});
break;
}
}
//-------------------------------------------------------------------
// Event handler for overall graph.
//-------------------------------------------------------------------
function graph_events(e) {
var m1 = $('#timeline .marker:nth(0)');
var mr = $('#timeline .hilight rect')
var m2 = $('#timeline .marker:nth(1)');
var hinfo = $('#graph-bar-info');
switch (e.type) {
//-----------------------------
case 'mousemove':
e.stopPropagation();
var x = e.offsetX - graph_width_left;
if ((e.offsetY > graph_height_top || x < 0 || x >= graph_width) && graph_hilight_mode < 2) {
graph_hilight_mode = 0;
m1.attr('transform','translate(0 -1000)');
m2.attr('transform','translate(0 -1000)');
mr.attr('y','-1000');
break;
}
var slot = Math.round(x / graph_width_bar);
if (slot < 0) slot = 0;
if (slot > graph_hours*4) slot = graph_hours*4;
x = Math.floor(slot * graph_width_bar) + graph_width_left;
switch (graph_hilight_mode) {
//-----------------------------
case 0: // Idle mode, but mouse just entered the area for selecting range.
graph_hilight_mode++;
// Fall-through
//-----------------------------
case 1: // User is inside the area for selecting range. Display 'start' marker.
graph_range_slot1 = graph_range_slot2 = slot;
graph_hilight_x1 = graph_hilight_x2 = x;
m1.attr('transform','translate('+x+' '+graph_height_top+')');
break;
//-----------------------------
case 2: // User is dragging a range selection.
if (graph_range_slot2 === slot) break;
m2.attr('transform','translate('+x+' '+graph_height_top+')');
graph_range_slot2 = slot;
graph_hilight_x2 = x;
mr.attr('x',Math.min(graph_hilight_x1,graph_hilight_x2)).attr('y',graph_height_top)
mr.attr('width',Math.floor(Math.abs(graph_hilight_x2-graph_hilight_x1)));
populate_info(graph_range_slot1, graph_range_slot2, !show_detail_while_dragging);
break;
}
break;
//-----------------------------
case 'mousedown':
if (e.button != 0) break; // Only left mouse button
switch (graph_hilight_mode) {
//-----------------------------
case 1: // User is in area for selecting range, and just clicked to start selecting.
graph_hilight_mode = 2;
e.preventDefault();
e.stopPropagation();
var timeln_x = $('#timeline').offset().left;
open_modal('mousemove mousedown mouseup', function(e) {
e.offsetX -= timeln_x;
graph_events(e);
});
break;
//-----------------------------
case 2: // User clicked for 'end' range. (No longer used, since only click-drag-release is supported.)
$('#timeln-modal').css('z-index',1);
graph_hilight_mode = 3;
populate_info(graph_range_slot1, graph_range_slot2, false);
break;
//-----------------------------
case 3: // Range was already selected. Either close existing range, or start new range.
graph_hilight_mode = 0;
m1.attr('transform','translate(0 -1000)');
m2.attr('transform','translate(0 -1000)');
mr.attr('y','-1000');
hinfo.addClass('hidden');
close_modal();
// If user clicked again on timeline bar, start new range selection.
var t = $('#timeline');
var tx1 = graph_width_left;
var tx2 = tx1 + graph_width;
var ty1 = t.offset().top;
var ty2 = ty1 + graph_height_top;
var cx = e.offsetX;
var cy = e.offsetY;
if (cx >= tx1 && cx < tx2 && cy >= ty1 && cy < ty2) {
graph_hilight_mode = 1;
e.target = t[0];
e.offsetY -= t.offset().top;
e.type = 'mousemove';
graph_events(e);
e.type = 'mousedown';
graph_events(e);
} else {
}
break;
}
break;
//-----------------------------
case 'mouseup':
if (e.button != 0) break; // Only left mouse button
if (graph_hilight_mode != 2) break; // Only process release during drag.
// Check if user dragged, or only clicked.
if (graph_range_slot1 !== graph_range_slot2) {
$('#timeln-modal').css('z-index',1);
graph_hilight_mode = 3;
populate_info(graph_range_slot1, graph_range_slot2, false);
} else {
graph_hilight_mode = 1;
m2.attr('transform','translate(0 -1000)');
mr.attr('y','-1000');
hinfo.addClass('hidden');
close_modal();
}
break;
//-----------------------------
case 'mouseleave':
// User wasn't in the process of selecting a range, and the mouse
// left the area for selecting a range. Hide 'start' marker.
if (graph_hilight_mode < 2) {
m1.attr('transform','translate(0 -1000)');
m2.attr('transform','translate(0 -1000)');
mr.attr('y','-1000');
graph_hilight_mode = 0;
}
break;
}
}
//-------------------------------------------------------------------
// Event handler for hours slider.
//-------------------------------------------------------------------
function change_hours(e) {
graph_hours = Math.round(Number($('#range_input').val())*24);
localStorage.setItem('timeln_graph_hours', graph_hours);
$('#range_days').text(slider_label(graph_hours));
if (e.type === 'change' || get_setting('rescale_redraw'))
draw_timeline();
}
//-------------------------------------------------------------------
// Draw the timeline.
//-------------------------------------------------------------------
function draw_timeline() {
// Need to 'restore' before redrawing.
if (get_setting('minimized')) $('#timeln').removeClass('min');
// Do some cleanup, in case redraw was triggered by 15min timer.
$('#graph-bar-info, #graph-bar-info').addClass('hidden');
close_modal();
graph_hilight_mode = 0;
// Update our timeline data based on cache.
calc_timeline();
// If cache says we have available items, but WK says next review
// date is in the future, user must have done reviews on another
// device. Need to force refresh.
var now = Math.floor(new Date()/1000);
if (first_draw === true && timeline[0] !== undefined && next_review >= Math.ceil(now/900)*900) {
first_draw = false;
setTimeout(click_refresh, 50); // Refresh after finishing main()
return;
}
// Update slider label with number of reviews on graph.
$('#range_reviews').text(graph_review_total);
// Calculate graph dimensions.
var timeln_graph = $('#timeln-graph');
$('#timeline').remove();
graph_height_panel = timeln_graph.height();
graph_height = graph_height_panel - (graph_height_top + graph_height_bottom);
graph_width_panel = timeln_graph.width();
graph_width = graph_width_panel - graph_width_left;
// String for building html.
var grid = '';
var label_x = '';
var label_y = '';
var bars = '';
var arrows = '';
// Calculate major and minor vertical graph tics.
var inc_s = 1, inc_l = 5;
while (Math.ceil(max_reviews / inc_s) > 5) {
switch (inc_s.toString()[0]) {
case '1': inc_s *= 2; inc_l *= 2; break;
case '2': inc_s = Math.round(2.5 * inc_s); break;
case '5': inc_s *= 2; inc_l *= 5; break;
}
}
// Draw vertical graph tics (# of Reviews).
var tic, tic_class, y;
graph_reviews = Math.ceil(max_reviews / inc_s) * inc_s;
for (tic = 0; tic <= graph_reviews; tic += inc_s) {
tic_class = ((tic % inc_l) === 0 ? 'major' : 'minor');
y = (graph_height_top + graph_height) - Math.round(graph_height * (tic / graph_reviews));
if (tic > 0)
grid += '';
label_y += ''+tic+'';
}
// Set up to draw horizontal graph tics (Time).
var major_tic_choices = [1, 3, 6, 12, 24]; // Hours
var minor_tic_choices = [1, 4, 4, 12, 24]; // 15min intervals
var max_labels = Math.floor(graph_width / 50); // No more than 1 label every 50 pixels
var tic_choice = 0;
while ((graph_hours / major_tic_choices[tic_choice]) > max_labels && tic_choice < major_tic_choices.length) tic_choice++;
var major_tic = major_tic_choices[tic_choice] * 4;
var minor_tic = minor_tic_choices[tic_choice];
// Draw grid tics, and populate datapoints
var tic_ofs = Math.floor((calc_time - (new Date(calc_time)).setHours(0, 0, 0, 0)) / 900000);
graph_width_bar = (graph_width-1) / (graph_hours*4); // Width of a time slot.
for (tic = 0; tic <= graph_hours*4; tic++) {
var x = Math.floor(graph_width_left + tic * graph_width_bar);
// Need to use date function to account for time shifts (e.g. Daylight Savings Time)
tstamp = new Date(calc_time + tic * 900000);
var hh = tstamp.getHours();
var qh = hh*4 + Math.round(tstamp.getMinutes()/15);
// Check if we are on a Major Tic mark
if (qh % major_tic === 0) {
// Start of a new day?
if (hh === 0) {
tic_class = 'newday';
label = 'SunMonTueWedThuFriSat'.substr(tstamp.getDay()*3, 3);
} else {
tic_class = 'major';
tstamp = new Date(calc_time + tic * 900000);
var hh = tstamp.getHours();
if (settings['24hour']) {
label = ('0'+hh+':00').slice(-5);
} else {
label = (((hh + 11) % 12) + 1) + 'ap'[Math.floor(hh/12)] + 'm';
}
}
if (tic > 0)
grid += '';
label_x += ''+label+'';
} else if (qh % minor_tic === 0) {
// Minor Tic mark
if (tic > 0)
grid += '';
}
// If there are reviews for the current timeslot, draw graph bars.
var slot = timeline[tic];
if (slot && tic < graph_hours*4) {
var x1 = x - graph_width_left;
var x2 = Math.floor((tic+1) * graph_width_bar);
var base = 0;
var rad = slot.radicals.length;
var kan = slot.kanji.length;
var voc = slot.vocabulary.length;
if (get_setting('summary_bar_only')) {
bars += '';
base += rad+kan+voc;
} else {
if (rad > 0) {
bars += '';
base += rad;
}
if (kan > 0) {
bars += '';
base += kan;
}
if (voc > 0) {
bars += '';
base += voc;
}
}
// If current timeslot has current-level items, or items ready for Burning, draw indicator arrows.
var ay = graph_height_top+graph_height+1;
var ac = graph_width_left+(x1+x2)/2;
var ah = 7;
var aw = Math.min(x2-x1, ah);
var ax1 = ac-(aw/2);
var ax2 = ax1+aw;
if (slot.has_current) {
if (get_setting('show_current_bars'))
bars += '';
arrows += '';
}
ay += ah+1;
if (slot.has_burn) {
if (get_setting('show_burn_bars') && !(slot.has_current && get_setting('show_current_bars')))
bars += '';
arrows += '';
}
bars += '';
}
}
// Build the html/svg object from the components build above.
timeln_graph.append(
''
);
// Add event handlers for the graph.
$('#timeline .bars .clr').on('mousemove mouseleave click', bar_events);
$('#timeline').on('mousemove mousedown mouseup mouseleave', graph_events);
// Schedule next timeline update, 1sec after next qtr hour.
var next_time = (new Date()).getTime();
next_time = Math.ceil(next_time/900000)*900000 - next_time + 1000;
setTimeout(function() {
draw_timeline();
}, next_time);
// Need to 'restore' before redrawing.
if (get_setting('minimized')) $('#timeln').addClass('min');
}
//-------------------------------------------------------------------
// Generate timeline data.
//-------------------------------------------------------------------
function calc_timeline() {
calc_time = new Date();
calc_time = calc_time.setMinutes(Math.floor(calc_time.getMinutes()/15)*15, 0, 0);
var next_time = Math.ceil(calc_time/900000); // Timestamp of next 15min slot
max_reviews = 3;
graph_review_total = 0;
var max_slot = graph_hours*4;
timeline = [];
types = ['radicals', 'kanji', 'vocabulary'];
var mark = {
radicals: (get_setting('mark_current') === true),
kanji: (get_setting('mark_current') === true),
vocabulary: ((get_setting('mark_current') === true) && (get_setting('mark_current_vocab') === true))
};
for (var type_idx in types) {
var type = types[type_idx];
var item_cnt = user_data[type].length;
var mark_current = mark[type];
for (var item_idx = 0; item_idx < item_cnt; item_idx++) {
var item = user_data[type][item_idx];
var item_time = Math.floor(item.user_specific.available_date / 900); // Round down to 15min.
var slot_idx = Math.min(max_slot, Math.max(0, item_time - next_time));
if (timeline[slot_idx] === undefined)
timeline[slot_idx] = {radicals:[], kanji:[], vocabulary:[], item_count:0, has_current:false, has_burn:false, item_time:item_time*900};
var slot = timeline[slot_idx];
slot.item_count++;
if (slot_idx < max_slot) {
graph_review_total++;
if (slot.item_count > max_reviews)
max_reviews = slot.item_count;
}
slot[type].push(item);
if (mark_current && item.level == user_level)
slot.has_current = true;
if (item.user_specific.srs_numeric == 8)
slot.has_burn = true;
}
}
gobj.timeline = timeline;
}
//-------------------------------------------------------------------
// Make first letter of each word upper-case.
//-------------------------------------------------------------------
function toTitleCase(str) {
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}
//-------------------------------------------------------------------
// Add a '+
'
Reviews Timeline
'+
' '+
' '+
' '+
' '+
' '+
' '+
' '+
' '+
'
'+
''
);
$('#timeln-open, #timeln-minimize').on('click', function(e){
e.preventDefault();
set_setting('minimized', !get_setting('minimized'));
$('#timeln').toggleClass('min');
});
$('#timeln-settings-lnk').on('click', click_settings);
$('#timeln-refresh-lnk').on('click', click_refresh);
$('#timeln-help-lnk').on('click', click_help);
$('#timeln-graph').height(Number(get_setting('graph_height')));
}
// Gather some info to help determine cache status.
user_level = Number($('.levels span:nth(0)').text());
next_review = Number($('.review-status .timeago').attr('datetime'));
last_review = Number(localStorage.getItem('timeln_last_review') || 0);
last_unlock = new Date($('.recent-unlocks time:nth(0)').attr('datetime'))/1000;
last_fetch = Number(localStorage.getItem('timeln_last_fetch') || 0);
// 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;
}
// Fetch API key and update cache, only if necessary.
var promise;
status_div = $('#timeln-status');
if (last_fetch <= last_unlock || last_fetch <= last_review || (next_review < now && last_fetch <= (now-3600))) {
status_div.removeClass('hidden');
status_div.html('Failed to fetch API key!');
promise = get_api_key()
.then(get_review_data)
.catch(function(e){status_div.html(e.message);});
} else {
promise = Promise.resolve();
}
// We have an up-to-date cache. Draw the timeline.
promise.then(function(){
// Fetch user data from cache.
user_data = JSON.parse(localStorage.getItem('timeln_cache'));
// Draw the timeline.
status_div.addClass('hidden');
draw_timeline();
// Install event handlers for time range slider.
$('#range_input').on('input change', change_hours);
$('#range_form').removeClass('hidden');
},null);
}
// Run startup() after window.onload event.
if (document.readyState === 'complete')
startup();
else
window.addEventListener("load", startup, false);
})(wktimeln);