WaniKani Forums: Like counter

Keeps track of the likes you've used and how many you have left... supposedly.

La data de 23-07-2020. Vezi ultima versiune.

// ==UserScript==
// @name         WaniKani Forums: Like counter
// @namespace    http://tampermonkey.net/
// @version      2.0.9
// @description  Keeps track of the likes you've used and how many you have left... supposedly.
// @author       Kumirei
// @include      https://community.wanikani.com*
// @require      https://greatest.deepsurf.us/scripts/5392-waitforkeyelements/code/WaitForKeyElements.js
// @grant        none
// ==/UserScript==

(function() {
    // SETTINGS
    var settings = {
        hr12: false, // Show times in 12hr format
        displayLikesReceived: true, // Display likes received counter
        updateInterval: 10, // Interval (minutes) for fetching summary page data
        lifetimePurple: false, // Set to true for purple info bubbles
    }

    // Global variable
    var LC = {
        lastUpdate: 0,
        likes: [],
        likesGiven: 0,
        maxGiven: 0,
        maxLikes: 0,
        likesReceived: [],
        ranOut: 0,
        ranOutDate: Date.now(),
        fullLikes: 0,
        fullLikesDate: Date.now(),
        daysVisited: 0,
    }

    // MAIN
    const msday = 1000*60*60*24 // Number of ms in a day
    var $ = window.$;
    init();

    // First run setup
    function init() {
        setInitialiseTriggers();
        addDisplay();
        initialise();
        timeLikes();
        setTimeout(updateDisplay, 1000*60);
        addCSS();
    }

    // Set triggers for when the page needs to be initialised again
    function setInitialiseTriggers() {
        // When loading the window
        window.addEventListener('load', initialise);
        // When using back and forth buttons
        window.addEventListener('popstate', initialise);
        // When navigating
        (function(history){
            var pushState = history.pushState;
            history.pushState = function(state) {
                initialise();
                return pushState.apply(history, arguments);
            };
        })(window.history);
    }

    // Set up the page for continued running
    function initialise() {
        fetchData();
        detectLikes();
    }

    // Fetches the needed data
    function fetchData() {
        updateLC();

        if (LC.lastUpdate < Date.now() - 1000*60*settings.updateInterval) {
            LC.lastUpdate = Date.now();
            save();
            var username = $('#current-user a').attr('href').split('/u/')[1];
            var url = 'https://community.wanikani.com/u/'+username+'/summary';
            $.ajax({
                url: url,
                type: 'GET',
                dataType: 'json',
                success: processData
            });
        }
    }

    // Updates the global variable with the new data
    function processData(data) {
        var likesGiven = data.user_summary.likes_given;
        if (likesGiven > LC.likesGiven && LC.likesGiven != 0) registerLikes(likesGiven - LC.likesGiven);
        LC.likesGiven = likesGiven;

        var trustID = data.badges[0].id;
        LC.maxLikes = 50*(1+trustID);

        var likesReceived = data.user_summary.likes_received;
        LC.likesReceived.push([likesReceived, Date.now()]);

        LC.daysVisited = data.user_summary.days_visited;

        save();
        updateDisplay();
    }

    // Update the LC variable
    function updateLC() {
        var stored = JSON.parse(localStorage.getItem('WKFLC'));
        if (stored && stored.lastUpdate >= 1581692248858) LC = stored;
    }

    // Stores the data in localStorage so other tabs can access it
    function save() {
        localStorage.setItem('WKFLC', JSON.stringify(LC));
    }

    // Adds the bubbles to the header
    function addDisplay() {
        // START code by rfindley
        if (is_dark_theme()) {
            $('body').attr('theme','dark');
        } else {
            $('body').attr('theme','light');
        }
        var wk_app_nav = $('.wanikani-app-nav').closest('.container');
        if (wk_app_nav.length === 0) {
            setTimeout(addDisplay, 200);
            return;
        }
        // Attach the Dashboard menu to the stay-on-top menu.
        var top_menu = $('.d-header');
        var main_content = $('#main-outlet');
        $('body').addClass('float_wkappnav');
        wk_app_nav.addClass('wanikani-app-nav-container');
        top_menu.find('>.wrap > .contents:eq(0)').after(wk_app_nav);
        // Adjust the main content's top padding, so it won't be hidden under the new taller top menu.
        var main_content_toppad = Number(main_content.css('padding-top').match(/[0-9]*/)[0]);
        main_content.css('padding-top', (main_content_toppad + 25) + 'px');
        // Insert CSS.
        var css =
            '.float_wkappnav .d-header {height:inherit;}'+
            '.float_wkappnav .d-header .title {height:4em;}'+
            '.float_wkappnav .wanikani-app-nav-container {border-top:1px solid #ccc; line-height:2em;}'+
            '.float_wkappnav .wanikani-app-nav ul {padding-bottom:0; margin-bottom:0; border-bottom:inherit;}'+
            '.dashboard_bubble {color:#fff; background-color:#bdbdbd; font-size:0.8em; border-radius:0.5em; padding:0 6px; margin:0 0 0 4px; font-weight:bold;}'+
            'li[data-highlight="true"] .dashboard_bubble {background-color:#6cf;}'+
            'body[theme="dark"] .dashboard_bubble {color:#ddd; background-color:#646464;}'+
            'body[theme="dark"] li[data-highlight="true"] .dashboard_bubble {color:#000; background-color:#6cf;}'+
            'body[theme="dark"] .wanikani-app-nav[data-highlight-labels="true"] li[data-highlight="true"] a {color:#6cf;}'+
            'body[theme="dark"] .wanikani-app-nav ul li a {color:#999;}';
        $('head').append('<style type="text/css">'+css+'</style>');
        // END code by rfindley
        if (settings.displayLikesReceived) {
            $('.wanikani-app-nav ul').append('<li data-highlight="false">Likes Received<span id="likes_received" class="dashboard_bubble">0</span></li>');
        }
        $('.wanikani-app-nav ul').append('<li data-highlight="false">Likes Left<span id="likes_left" class="dashboard_bubble">0</span></li>');
        $('.wanikani-app-nav ul').append('<li data-highlight="false">Next Like<span id="next_like" class="dashboard_bubble">0</span></li>');
        updateDisplay();
    }

    // Function made by rfindley
    function is_dark_theme() {
        // Grab the <html> background color, average the RGB.  If less than 50% bright, it's dark theme.
        return $('html').css('background-color').match(/\((.*)\)/)[1].split(',').slice(0,3).map(str => Number(str)).reduce((a, i) => a+i)/(255*3) < 0.5;
    }

    // Updates the display with the current numbers
    function updateDisplay() {
        fetchData();
        updateLC();
        pruneLikes();
        if (LC.likes.length > LC.maxGiven) LC.maxGiven = LC.likes.length;
        save();

        var likesLeft = LC.maxLikes - LC.likes.length;
        var likesLeftDisplay = (likesLeft < 0 ? 0 : likesLeft);
        var nextLike = timeLeft(LC.likes[0]+msday);
        var first = LC.likesReceived[0];
        var last = LC.likesReceived[LC.likesReceived.length-1];
        var likesReceived = [(first?first:[0])[0], (last?last:[0])[0]]; // Total received yesterday and today
        var receivedToday = likesReceived[1] - likesReceived[0];
        var receivedTooltip = receivedToday + ' likes received in past day\n' + comma(likesReceived[1]) + ' total likes received';
        var nextHour = findNext(msday/24);
        var nextHourTooltip = nextHour + ' likes in next hour\nNext like at ' + parseTime(LC.likes[0]);
        var dailyAverage = likesReceived[1]/LC.daysVisited;
        var ranOutTooltip = LC.ranOut + ' times have you ran out\n' + ((Date.now()-LC.ranOutDate)/msday).toFixed(0) +
            ' days since you ran out\n';
        if (LC.ranOut == 0) ranOutTooltip = LC.maxGiven + ' likes most given in a day\n';
        var likesLeftTooltip = dailyAverage.toFixed(0)+' likes given per day on average\n' + comma(LC.likesGiven) + ' total likes given \n\n' + ranOutTooltip + LC.fullLikes + ' times have you had full likes';;

        if ($('#likes_left').length) {
            if (settings.displayLikesReceived) {
                toggleHighlight($('#likes_received'), receivedToday != 0);
                $('#likes_received')[0].innerHTML = receivedToday;
                $('#likes_received').closest('li').attr('title', receivedTooltip);
                $('#likes_received').closest('li').attr('data-name', "likes-received");
            }
            $('#likes_left')[0].innerHTML = likesLeftDisplay;
            $('#next_like')[0].innerHTML = nextLike;
            toggleHighlight($('#likes_left'), likesLeft != 0);
            toggleHighlight($('#next_like'), nextLike != 'N/A');
            $('#next_like').closest('li').attr('title', nextHourTooltip);
            $('#next_like').closest('li').attr('data-name', "likes-next");
            $('#likes_left').closest('li').attr('title', likesLeftTooltip);
            $('#likes_left').closest('li').attr('data-name', "likes-left");
        }
    }

    // Deletes any likes older than 24 hours
    function pruneLikes() {
        $(LC.likes).each((i, time)=>{
            if (time < Date.now() - msday) LC.likes.splice(0, 1);
            else return false;
        });
        $(LC.likesReceived).each((i, entry)=>{
            if (entry[1] < Date.now() - msday) LC.likesReceived.splice(0, 1);
            else return false;
        });
    }

    // Toggles whether the info bubbles should be coloured or grey
    function toggleHighlight(e, on) {
        e.closest('li').attr('data-highlight', on);
        e.toggleClass('zero', on);
    }

    // Set a timer for each like so that they can count down the seconds
    function timeLikes() {
        for (var i=0; i<LC.likes.length; i++) timeLike(LC.likes[i]);
    }

    // Sets a timer for one like to count down its last seconds
    function timeLike(time) {
        var timestamp = time+msday-1000*60;
        var timeLeft = timestamp - Date.now();
        if (timeLeft < 0) timeLeft = 0;
        setTimeout(()=>{
            var s = Math.round((time+msday-Date.now())/1000);
            var interval = setInterval(()=>{
                var currentSeconds = $('#next_like')[0].innerHTML.match(/^\d{1,2}s$/);
                if (!currentSeconds || currentSeconds[0].slice(0, -1) > s) {
                    $('#next_like')[0].innerHTML = s+'s';
                    if (s<=0) {
                        clearInterval(interval);
                        updateDisplay();
                    }
                }
                s--;
            }, 1000);
        }, timeLeft);
    }

    // Sets up detection of new likes
    function detectLikes() {
        waitForKeyElements('#topic-bottom', ()=>{
            $('.post-stream').on('click', '.toggle-like:not(.has-like)', (event)=>{
                registerLikes(1);
                // Check whether the like went through and if it didn't remove the registered like.
                let post_id = event.target.closest('article').getAttribute('data-post-id');
                let url = window.location.href;
                setTimeout(()=>{
                    $.ajax({
                        url: url,
                        type: 'GET',
                        dataType: 'json',
                        success: (data)=>{
                            for (let post of data.post_stream.posts) {
                                if (post.id == post_id) {
                                    for (let action of post.actions_summary) {
                                        if (action.id === 2) {
                                            if (!action.acted) registerLikes(-1);
                                            break;
                                        }
                                    }
                                    break;
                                }
                            }
                        }
                    });
                }, 1000);
                updateDisplay();
            });
        });
    }

    // Registers new likes used
    function registerLikes(count) {
        updateLC();
        if (LC.likes.length == 0 && LC.fullLikesDate < Date.now() - msday) {
            LC.fullLikes++;
            LC.fullLikesDate = Date.now();
        }
        LC.likesGiven += count;
        if (count == -1) LC.likes.pop();
        else {
            for (var i=0; i<count; i++) {
                LC.likes.push(Date.now());
                timeLike(Date.now());
            }
        }
        if (LC.likes.length >= LC.maxLikes && LC.ranOutDate < Date.now() - msday) {
            LC.ranOut++;
            LC.ranOutDate = Date.now();
        }
        save();
        updateDisplay();
    }

    // Returns a string with the time remaining until the given date
    function timeLeft(date) {
        if (!date) return 'N/A';
        var seconds = (date - Date.now())/1000;
        var s = Math.floor(seconds % 3600 % 60);
        var sr = Math.round(seconds % 3600 % 60);
        var m = Math.floor((seconds-s)/60%60);
        var mr = Math.round((seconds-s)/60%60);
        var h = Math.floor((seconds-s-m*60)/3600);
        var hr = Math.round((seconds-s-m*60)/3600);
        if (h != 0) return hr + 'h';
        if (m != 0) return mr + 'm';
        if (s != 0) return sr + 's';
    }

    // Finds the number of likes expiring in the next interval of time
    function findNext(interval) {
        var count = 0;
        for (var i=0; i<LC.likes.length; i++) {
            if (LC.likes[i] + msday < Date.now() + interval) count++;
        }
        return count;
    }

    // Adds the CSS
    function addCSS() {
        let bubbleColor = settings.lifetimePurple ? 'rgb(213, 128, 255)' : '#6cf';
        $('head').append('<style id=like_counter>'+
                         '    .wanikani-app-nav ul li {color: #545454;}'+
                         '    body[theme="dark"] .wanikani-app-nav ul li {color:#999;}'+
                         '    li[data-highlight="true"] span.dashboard_bubble {background-color: '+bubbleColor+';}'+
                         '    .wanikani-app-nav > ul {display: flex;}'+
                         '    .wanikani-app-nav li[data-name="likes-received"] {order: 1;}'+
                         '    .wanikani-app-nav li[data-name="likes-left"] {order: 2;}'+
                         '    .wanikani-app-nav li[data-name="likes-next"] {order: 3;}'+
                         '</style>'
                        );
    }

    // Creates a timestamp for the given time
    function parseTime(date) {
        if (!date) return 'N/A';
        var time = new Date(date);
        var h = time.getHours();
        var m = time.getMinutes();
        var AmPm = '';
        if (settings.hr12) {
            AmPm = ' AM';
            if (h > 11) {
                AmPm = ' PM';
                if (h != 12) h -= 12;
            }
            else if (h == 0) h = 12;
        }
        else if (h < 10) h = '0' + h;
        if (m < 10) m = '0' + m;
        return h+':'+m+AmPm;
    }

    // Adds commas to a number
    function comma(x) {
        return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
    }
})();