WaniKani Forums: Like counter

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

À partir de 2020-06-24. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         WaniKani Forums: Like counter
// @namespace    http://tampermonkey.net/
// @version      2.0.6
// @description  Keeps track of the likes you've used and how many you have left... supposedly.
// @author       Kumirei
// @include      https://community.wanikani.com*
// @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_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);
            $('#likes_left').closest('li').attr('title', likesLeftTooltip);
        }
    }

    // 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)', ()=>{
                registerLikes(1);
                setTimeout(()=>{ // If like did not go through delete it
                    // This is admittedly a faulty way to detect whether a like went through, but until Discourse fixes the bug
                    // where the heart stays filled even when the like did not go through it will have to do
                    if ($('.bootbox').length) {
                        if ($('.bootbox .modal-body')[0].innerText.includes('reached the maximum number of likes')) {
                            registerLikes(-1);
                        }
                    }
                },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+' !important;}'+
                         '</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, ",");
    }
})();