WaniKani Timeline

This is the WaniKani Customizer Chrome Extension Timeline ported to UserScript

目前为 2015-01-31 提交的版本。查看 最新版本

// ==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');
}());

QingJ © 2025

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