WaniKani Timeline

This is the WaniKani Customizer Chrome Extension Timeline ported to UserScript

目前為 2015-01-31 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name          WaniKani Timeline
// @namespace     https://www.wanikani.com
// @description   This is the WaniKani Customizer Chrome Extension Timeline ported to UserScript
// @version       0.1.2
// @include       https://www.wanikani.com/
// @include       https://www.wanikani.com/dashboard
// @include       https://www.wanikani.com/account
// @run-at        document-end
// @grant         none
// ==/UserScript==

/*jslint browser: true, plusplus: true*/
/*global $, console, alert, confirm */

/*
Almost all of this code is by Kyle Coburn aka kiko on WakiKani.
It has been reformatted slightly and some minor changes made.
rev 0.1.1: added a few more options
for classic style: enable only fuzzy_time_mode_past, fuzzy_time_mode_near, and optionally twelve_hour_mode
rev 0.1.2:
change to fix server timeouts on higher level vocabulary
change to prevent always reloading data when no current reviews
increased max display time to 7-days
changed data storage format. now stores number and type of current and burning items. (for more display options later)
added a last updated time (works with 'WaniKani Real Times')
added a basic loading and error indicator
added 'wkt_username_' prefix to localStorage keys (for multiple WK account support)
added a reload button
added invalid API key detection. forget bad key. get new on refresh.
*/

(function () {
    'use strict';
    var level, localStoragePrefix, tRes, ajaxCompletedCount, api_calls, api_colors, curr_date, start_time, gHours, graphH, graphH_canvas, styleCss,
        xOff, vOff, max_hours, times, pastReviews, firstReview, tFrac, page_width, options = {};
    /* ### CONFIG OPTIONS ### */
    // options.twelve_hour_mode = true; // enable 12-hour AM/PM mode
    options.fuzzy_time_mode_past = true; // enable '-x mins' mode for items now available
    // options.fuzzy_time_mode_near = true; // enable 'x mins' mode for upcoming items: now < time < now+90min
    options.fuzzy_time_paren = true; // append (x mins) to time for items: time < now+90min
    options.show_weekday = true; // show weekday prefix
    options.enable_arrows = true; // enable indicator arrows
    /* ### END CONFIG ### */

    api_colors = ['#0096e7', '#ee00a1', '#9800e8'];
    curr_date = new Date();
    start_time = curr_date.getTime() / 1000;
    gHours = 18;
    graphH = 88;
    graphH_canvas = graphH + 15;
    xOff = 18;
    vOff = 16;
    max_hours = 24 * 7;

    function strNumSeq(min, max) {
        var i, str = '';
        for (i = min; i <= max; i++) {
            if (str) {
                str += ',';
            }
            str += i;
        }
        return str;
    }
    function addSplitVocab(level) {
        var segCnt, segLen, min, max,
            vocabRequestLevelSplitSize = 20; // maximum number of levels per vocab API request
        segCnt = Math.ceil(level / vocabRequestLevelSplitSize);
        segLen = Math.ceil(level / segCnt);
        for (min = 1; min <= level; min += segLen) {
            max = min + segLen - 1;
            if (max > level) {
                max = level;
            }
            api_calls.push('vocabulary/' + strNumSeq(min, max));
        }
    }
    function getDashboardLevel() {
        var match, levelStr = $('section.progression h3').html();
        if (levelStr) {
            match = levelStr.match(/Level (\d+) /);
            if (match && match.length === 2) {
                return parseInt(match[1], 10);
            }
        }
        return null;
    }
    api_calls = ['radicals', 'kanji'];
    level = getDashboardLevel();
    if (level && 0 < level && level < 100) { // allow for level expansion
        addSplitVocab(level);
    } else { // if unknown level fail to no-split
        api_calls.push('vocabulary');
    }

    function getPageUser() {
        var match, profileUrl = $('ul.nav a:contains("Profile")').prop('href');
        if (profileUrl) {
            match = profileUrl.match('[^/]*$');
            if (match && match.length === 1) {
                return match[0];
            }
        }
        return ''; // blank if error
    }
    localStoragePrefix = 'wkt_' + getPageUser() + '_';

    // Helpers
    function pluralize(noun, amount) {
        return amount + ' ' + (amount !== 1 ? noun + 's' : noun);
    }
    function fuzzyMins(minutes) {
        var seconds;
        if (minutes < 1 && minutes > 0) {
            seconds = Math.round(minutes * 60);
            return pluralize('sec', seconds);
        }
        minutes = Math.round(minutes);
        return pluralize('min', minutes);
    }
    // Draw
    function drawBarRect(ctx, xo, yo, bw, bh, color) {
        ctx.fillStyle = color;
        ctx.fillRect(xo, yo, bw, bh);
    }
    function drawBar(ctx, time, height, hOff, color, tFrac, outlined) {
        var bx = xOff + time * tFrac, by = graphH - height - hOff;
        ctx.fillStyle = color;
        ctx.fillRect(bx, by, tFrac, height);
        if (outlined) {
            ctx.strokeStyle = (outlined === -1 ? '#ffffff' : '#000000');
            //ctx.strokeRect(bx, by, tFrac, hOff === 0 ? graphH : height);
            ctx.beginPath();
            ctx.moveTo(bx, by + height);
            ctx.lineTo(bx, by);
            ctx.lineTo(bx + tFrac, by);
            ctx.lineTo(bx + tFrac, by + height);
            if (hOff !== 0) { // don't draw stroke on top of axis
                ctx.lineTo(bx, by + height);
            }
            ctx.stroke();
        }
    }
    function drawArrow(ctx, currentType, xOff, time, tFrac) {
        if (currentType === -1) {
            ctx.fillStyle = '#FF0000';
        } else if (currentType === -2) {
            ctx.fillStyle = '#A0A0A0';
        } else {
            return;
        }
        var bx = xOff + (time - 1) * tFrac,
            topY = 3 + graphH,
            halfWidthX = tFrac / 2,
            cenX = bx + halfWidthX;
        if (halfWidthX > 9) { // limit arrow width
            halfWidthX = 9;
        }
        ctx.beginPath();
        ctx.moveTo(cenX, topY);
        ctx.lineTo(cenX - halfWidthX, topY + 10);
        ctx.lineTo(cenX + halfWidthX, topY + 10);
        ctx.fill();
        //ctx.lineTo(cenX, topY);
        //ctx.strokeStyle = "#000000";
        //ctx.stroke();
    }
    function drawCanvas(clear) {
        var totalCount, maxCount, graphTimeScale, ti, time, counts, total, ctx,
            gTip, pidx, canvasJQ, currentType,
            hrsFrac, gOff, height, count, i, width, hOff, xP,
            canvas = document.getElementById('c-timeline'),
            fuzzyExtra, weekdayText, weekday = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
        if (canvas.getContext) {
            totalCount = 0;
            maxCount = 3;
            graphTimeScale = 60 * 60 * (gHours - 0.1);
            if (gHours === 0) {
                if (pastReviews) {
                    for (ti = 0; ti < 3; ++ti) {
                        totalCount += pastReviews[ti][0];
                    }
                    maxCount = totalCount;
                }
            } else {
                for (time = 0; time < times.length; time++) {
                    if (time * 60 * tRes < graphTimeScale) {
                        counts = times[time];
                        if (counts) {
                            total = 0;
                            for (ti = 0; ti < 3; ++ti) {
                                total += counts[ti][0];
                            }
                            if (total > maxCount) {
                                maxCount = total;
                            }
                            totalCount += total;
                        }
                    }
                }
            }
            if (totalCount === 0) {
                maxCount = 0;
            }
            $('#g-timereviews').text(totalCount);
            tFrac = tRes * (page_width - xOff) / 60 / gHours;
            ctx = canvas.getContext("2d");
            if (clear) {
                ctx.clearRect(0, 0, page_width, graphH_canvas);
                page_width = $('.span12 header').width();
            } else {
                gTip = $('#graph-tip');
                canvasJQ = $('#c-timeline');
                canvas.addEventListener('mousemove', function (event) {
                    var reviewCount, currentCount, burnCount, idx, showTime, minDiff, tDisplay, tDate, hours, mins, suffix, tText;
                    if (gHours === 0) {
                        return;
                    }
                    //~ idx = Math.floor((event.offsetX - xOff) / tFrac) + 1;
                    idx = Math.floor(((event.pageX - canvasJQ.offset().left) - xOff) / tFrac) + 1;
                    if (idx !== pidx) {
                        counts = times[idx];
                        if (counts) {
                            gTip.show();
                            reviewCount = counts[0][0] + counts[1][0] + counts[2][0];
                            currentCount = counts[0][1] + counts[1][1] + counts[2][1];
                            burnCount = counts[0][2] + counts[1][2] + counts[2][2];
                            showTime = counts[3] * 1000;
                            minDiff = (showTime - new Date().getTime()) / 1000 / 60;
                            if (options.fuzzy_time_mode_past && minDiff < 0) {
                                tDisplay = fuzzyMins(minDiff);
                            } else if (options.fuzzy_time_mode_near && 0 <= minDiff && minDiff < 90) {
                                tDisplay = fuzzyMins(minDiff);
                            } else {
                                tDate = new Date(showTime);
                                hours = tDate.getHours();
                                mins = tDate.getMinutes();
                                suffix = '';
                                if (options.twelve_hour_mode) {
                                    suffix = ' ' + (hours < 12 ? 'am' : 'pm');
                                    hours %= 12;
                                    if (hours === 0) {
                                        hours = 12;
                                    }
                                }
                                if (hours < 10) {
                                    hours = '0' + hours;
                                }
                                if (mins < 10) {
                                    mins = '0' + mins;
                                }
                                weekdayText = '';
                                if (options.show_weekday) {
                                    weekdayText = weekday[tDate.getDay()] + ' ';
                                }
                                fuzzyExtra = '';
                                if (options.fuzzy_time_paren && minDiff < 90) {
                                    fuzzyExtra = '&nbsp;&nbsp;(' + fuzzyMins(minDiff) + ')';
                                }
                                tDisplay = weekdayText + hours + ':' + mins + suffix + fuzzyExtra;
                            }
                            tText = tDisplay + '<br />' + pluralize('review', reviewCount);
                            if (currentCount) {
                                tText += '<br /><em>' + currentCount + ' current level</em>';
                            }
                            if (burnCount) {
                                tText += '<br /><em>' + burnCount + ' burning</em>';
                            }
                            gTip.html(tText);
                            gTip.css({
                                left: canvasJQ.position().left + idx * tFrac + xOff,
                                top: event.pageY - gTip.height() - 6
                            });
                        } else {
                            gTip.hide();
                        }
                        pidx = idx;
                    } else {
                        gTip.css('top', event.pageY - gTip.height() - 6);
                    }
                }, false);
                canvasJQ.mouseleave(function () {
                    gTip.hide();
                    pidx = null;
                });
            }
            canvas.width = page_width;
            hrsFrac = gHours / 3;
            ctx.lineWidth = tFrac / 20;
            ctx.strokeStyle = "#ffffff";
            ctx.textBaseline = 'top';
            ctx.textAlign = 'right';
            ctx.font = '12px sans-serif';
            ctx.fillStyle = '#e4e4e4';
            if (gHours !== 0) {
                ctx.fillRect(0, Math.floor((vOff + graphH) * 0.5), page_width, 1);
            }
            ctx.fillRect(0, vOff - 1, page_width, 1);
            ctx.fillStyle = '#505050';
            ctx.textAlign = 'right';
            ctx.fillText(maxCount, xOff - 4, vOff + 1);
            // left border
            ctx.fillStyle = '#d4d4d4';
            ctx.fillRect(xOff - 2, 0, 1, graphH);
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(xOff - 1, 0, 1, graphH);
            // bottom border
            ctx.fillStyle = '#d4d4d4';
            ctx.fillRect(0, graphH, page_width, 1);
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(0, graphH + 1, page_width, 1);
            if (gHours === 0) {
                if (pastReviews) {
                    gOff = xOff;
                    height = graphH - vOff;
                    for (ti = 0; ti < 3; ++ti) {
                        count = pastReviews[ti][0];
                        if (count > 0) {
                            width = Math.ceil(count / maxCount * (page_width - xOff));
                            drawBarRect(ctx, gOff, vOff, width, height, api_colors[ti]);
                            gOff += width;
                        }
                    }
                }
            } else {
                for (i = 1; i < 4; ++i) {
                    xP = Math.floor(i / 3 * (page_width - 2));
                    if (i === 3) {
                        xP += 1;
                    } else if (page_width > 1100) {
                        --xP;
                    }
                    ctx.fillStyle = '#e4e4e4';
                    ctx.fillRect(xP, 0, 1, graphH_canvas);
                    ctx.fillStyle = '#505050';
                    ctx.fillText(String(hrsFrac * i), xP - 2, 0);
                }
                for (time = 0; time < times.length; time++) {
                    counts = times[time];
                    if (counts) {
                        hOff = 0;
                        currentType = 0;
                        if (counts[0][1] || counts[1][1] || counts[2][1]) {
                            currentType = -1;
                        } else if (counts[0][2] || counts[1][2] || counts[2][2]) {
                            currentType = -2;
                        }
                        if (currentType) {
                            drawBar(ctx, time - 1, graphH - vOff, 0, 'rgba(' + (currentType === -1 ? '255, 255, 255' : '0, 0, 0') + ', 0.33)', tFrac);
                        }
                        for (ti = 0; ti < 3; ++ti) {
                            count = counts[ti][0];
                            if (count > 0) {
                                height = Math.ceil(count / maxCount * (graphH - vOff));
                                drawBar(ctx, time - 1, height, hOff, api_colors[ti], tFrac, currentType);
                                hOff += height;
                            }
                        }
                        if (options.enable_arrows) {
                            drawArrow(ctx, currentType, xOff, time, tFrac);
                        }
                    }
                }
            }
        }
    }
    function displayUpdateTime(date) {
        var isoString, utcString;
        isoString = date.toISOString();
        utcString = isoString.replace(/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})\.\d{3}Z/, '$1 $2 UTC');
        $('section#r-timeline time').attr('datetime', isoString);
        $('section#r-timeline time').attr('title', 'Updated: ' + utcString);
        $('section#r-timeline time').html(utcString);
    }
    function initCanvas() {
        var reviewHours = Math.ceil(firstReview / 60 / 60 / 6) * 6;
        $('div#r-timeline-loading').hide();
        $('#r-timeline').show();
        if (reviewHours > gHours) {
            gHours = reviewHours;
            $('#g-range').attr('value', gHours); // set slider
            $('#g-range').trigger('change'); // update slider text / draw
        }
        drawCanvas(); // need to init hover, even if redraw
        displayUpdateTime(new Date(parseInt(localStorage.getItem(localStoragePrefix + 'cacheTime'), 10)));
    }

    function appendError(newError) {
        var error = $('span#r-timeline-loading-error').html();
        if (!error) {
            error = 'Error: ';
        } else {
            error += ', ';
        }
        error += newError;
        $('span#r-timeline-loading-error').html(error);
    }
    function ajaxError(xhr, ajaxOptions, thrownError) {
        appendError(thrownError);
    }
    // Load data
    function addData(data) {
        var itemIdx, response, myLevel, firstItem, typeIdx, maxSeconds, item, stats, availableAt, tDiff, timeIdx, timeTable;
        response = data.requested_information;
        if (!response) {
            if (data.error) {
                if (data.error.code === "user_not_found") {
                    appendError('badApiKey');
                    localStorage.removeItem(localStoragePrefix + 'apiKey'); // remove invalid key
                    return;
                }
            }
            appendError('badResponse');
            return;
        }
        if (response.general) {
            response = response.general;
        }
        myLevel = data.user_information.level;
        firstItem = response[0];
        typeIdx = firstItem.kana ? 2 : firstItem.important_reading ? 1 : 0;
        maxSeconds = 60 * 60 * max_hours;
        for (itemIdx = 0; itemIdx < response.length; itemIdx++) {
            item = response[itemIdx];
            stats = item.user_specific;
            if (stats && !stats.burned) {
                availableAt = stats.available_date;
                tDiff = availableAt - start_time;
                if (tDiff < maxSeconds) {
                    if (tDiff < firstReview) {
                        firstReview = tDiff;
                    }
                    timeIdx = tDiff < 1 ? -1 : Math.round(tDiff / 60 / tRes) + 1;
                    if (tDiff < 0) {
                        if (!pastReviews) {
                            pastReviews = []; // init object
                        }
                        timeTable = pastReviews;
                        availableAt = 0; // clear for past reviews
                    } else {
                        if (!times[timeIdx]) {
                            times[timeIdx] = []; // init object
                        }
                        timeTable = times[timeIdx];
                    }
                    if (!timeTable[0]) {
                        timeTable[0] = [0, 0, 0];   // 0:radical   [0:total, 1:current, 2:burn]
                        timeTable[1] = [0, 0, 0];   // 1:kanji
                        timeTable[2] = [0, 0, 0];   // 2:vocab
                        timeTable[3] = availableAt; // 3:time
                    } else if (availableAt < timeTable[3]) {
                        timeTable[3] = availableAt;
                    }
                    timeTable[typeIdx][0]++; // add item to r0/k1/v2 bin total
                    if (typeIdx < 2 && item.level === myLevel && stats.srs === 'apprentice') {
                        timeTable[typeIdx][1]++; // increment current
                    } else if (stats.srs === 'enlighten') {
                        timeTable[typeIdx][2]++; // increment burn
                    }
                }
            }
        }
        ajaxCompletedCount++;
        if (ajaxCompletedCount === api_calls.length && times && times.length > 0) {
            localStorage.setItem(localStoragePrefix + 'reviewCache', JSON.stringify(times));
            localStorage.setItem(localStoragePrefix + 'pastCache', JSON.stringify(pastReviews));
            localStorage.setItem(localStoragePrefix + 'cacheTime', curr_date.getTime());
            initCanvas();
        } else {
            $('span#r-timeline-loading-count').html(ajaxCompletedCount + '/' + api_calls.length);
            if (ajaxCompletedCount >= api_calls.length) {
                appendError('noData');
            }
        }
    }
    function insertTimeline() {
        var apiKey, cacheExpires, ext, idx, counts;
        ajaxCompletedCount = 0;
        apiKey = localStorage.getItem(localStoragePrefix + 'apiKey');
        if (apiKey && apiKey.length === 32) {
            $('section.review-status').before('<div id="r-timeline-loading">Reviews Timeline Loading: <span id="r-timeline-loading-count"></span> <span id="r-timeline-loading-error" style="color:red"></span></div><section id="r-timeline" style="display: none;"><h4>Reviews Timeline</h4><a class="help">?</a> <a class="reload" title="reload (clear timeline cache)">R</a> <time class="timeago"></time><form id="graph-form"><label><span id="g-timereviews"></span> reviews <span id="g-timeframe">in ' + gHours + ' hours</span> <input id="g-range" type="range" min="0" max="' + max_hours + '" value="' + gHours + '" step="6" name="g-ranged"></label></form><div id="graph-tip" style="display: none;"></div><canvas id="c-timeline" height="' + graphH_canvas + '"></canvas></section>');
            displayUpdateTime(new Date()); // normally overwritten later. workaround for 'Real Times' loading before data download.
            $('span#r-timeline-loading-count').html(ajaxCompletedCount + '/' + api_calls.length);
            try {
                times = JSON.parse(localStorage.getItem(localStoragePrefix + 'reviewCache'));
                pastReviews = JSON.parse(localStorage.getItem(localStoragePrefix + 'pastCache'));
            } catch (ignore) {}
            //~ if (times && pastReviews) {
            if (times) {
                cacheExpires = localStorage.getItem(localStoragePrefix + 'cacheTime');
                if (cacheExpires && curr_date - cacheExpires > 60 * 60 * 1000) {
                    times = null;
                }
            }
            //~ if (!times || !pastReviews || times.length === 0) {
            if (!times || times.length === 0) {
                times = null;
                pastReviews = null;
                localStorage.setItem(localStoragePrefix + 'reviewCache', null);
                localStorage.setItem(localStoragePrefix + 'pastCache', null);
                times = [];
                firstReview = Number.MAX_VALUE;
                for (ext = 0; ext < api_calls.length; ext++) {
                    $.ajax({
                        type: 'get',
                        url: '/api/v1.2/user/' + apiKey + '/' + api_calls[ext],
                        success: addData,
                        error: ajaxError
                    });
                }
            } else {
                for (idx = 0; idx < times.length; idx++) {
                    counts = times[idx];
                    if (counts) {
                        firstReview = counts[3] - start_time;
                        break;
                    }
                }
                setTimeout(initCanvas, 0);
            }
            $('a.help').click(function () {
                alert('Reviews Timeline - Displays your upcoming reviews\nY-axis: Number of reviews\nX-axis: Time (scale set by the slider)\n\nThe number in the upper left shows the maximum number of reviews in a single bar.\nWhite-backed bars indicate that review group contains radicals/kanji necessary for advancing your current level.');
            });
            $('a.reload').click(function () {
                if (confirm('Reviews Timeline: Reload Confirmation\n\nClick OK to clear the cache and refresh the page.\n\nWarning:\nExcessive API requests may be blocked by the server.') === true) {
                    localStorage.removeItem(localStoragePrefix + 'reviewCache');
                    document.location.reload();
                }
            });
            $('#g-range').change(function () {
                gHours = $(this).val();
                if (gHours < 6) {
                    gHours = pastReviews ? 0 : 3;
                }
                $('#g-timeframe').text(gHours === 0 ? 'right now' : 'in ' + gHours + ' hours');
                drawCanvas(true);
            });
        } else {
            alert('Hang on! We\'re grabbing your API key for the Reviews Timeline. We should only need to do this once.');
            document.location.pathname = '/account';
        }
    }
    // from: https://gist.githubusercontent.com/arantius/3123124/raw/grant-none-shim.js
    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;
    }
    styleCss = '\n' +
        '#graph-tip {\n' +
        '    padding: 2px 8px;\n' +
        '    position: absolute;\n' +
        '    color: #eeeeee;\n' +
        '    background-color: rgba(0,0,0,0.5);\n' +
        '    border-radius: 4px;\n' +
        '    pointer-events: none;\n' +
        '    font-weight: bold;\n' +
        '}\n' +
        'section#r-timeline {\n' +
        '    overflow: hidden;\n' +
        '    margin-bottom: 0px;\n' +
        '    height: ' + (options.enable_arrows ? '130' : '117') + 'px;\n' +
        '}\n' +
        'form#graph-form {\n' +
        '    float: right;\n' +
        '    margin-bottom: 0px;\n' +
        '    min-width: 50%;\n' +
        '    text-align: right;\n' +
        '}\n' +
        'section#r-timeline h4 {\n' +
        '    clear: none;\n' +
        '    float: left;\n' +
        '    height: 20px;\n' +
        '    margin-top: 0px;\n' +
        '    margin-bottom: 4px;\n' +
        '    font-weight: normal;\n' +
        '    margin-right: 12px;\n' +
        '}\n' +
        'a.help, a.reload {\n' +
        '    font-weight: bold;\n' +
        '    color: rgba(0, 0, 0, 0.1);\n' +
        '    font-size: 1.2em;\n' +
        '    line-height: 0px;\n' +
        '}\n' +
        'a.help:hover, a.reload:hover {\n' +
        '    text-decoration: none;\n' +
        '    cursor: help;\n' +
        '    color: rgba(0, 0, 0, 0.5);\n' +
        '}\n' +
        '@media (max-width: 767px) {\n' +
        '    section#r-timeline h4 {\n' +
        '        display: none;\n' +
        '    }\n' +
        '}\n' +
        '.dashboard section.review-status ul li time {\n' +
        '    white-space: nowrap;\n' +
        '    overflow-x: hidden;\n' +
        '    height: 1.5em;\n' +
        '    margin-bottom: 0;\n' +
        '}\n';
    if (document.location.pathname === "/account") {
        (function () {
            var apiKey, alreadySaved;
            apiKey = $('input[placeholder="Key has not been generated"]').val();
            if (apiKey) {
                alreadySaved = localStorage.getItem(localStoragePrefix + 'apiKey');
                localStorage.setItem(localStoragePrefix + 'apiKey', apiKey);
                console.log('WaniKani Timeline Updated API Key: ' + apiKey);
                if (!alreadySaved) {
                    document.location.pathname = '/dashboard';
                }
            }
        }());
    } else {
        page_width = $('.span12 header').width();
        if (page_width) {
            tRes = Math.round(1 / (page_width / 1170 / 15));
            insertTimeline();
        }
        addStyle(styleCss);
    }
    console.log('WaniKani Timeline: script load end');
}());