Greasy Fork is available in English.
Updates the review count automatically as soon as new reviews are due
// ==UserScript== // @name Wanikani: Real (Time) Numbers // @namespace http://tampermonkey.net/ // @version 1.2.2 // @description Updates the review count automatically as soon as new reviews are due // @author Kumirei // @include /^https://(www|preview).wanikani.com/(lesson/*|review/*|dashboard)?$/ // @grant none // ==/UserScript== /*jshint esversion: 8 */ // @include for /review/* and /lesson/* is required because otherwise, the script will not run on the /review summary // page if it was navigated to automatically upon completion of reviews/lesson (still works without them if navigating there directly) (function() { let script_name = "Real (Time) Numbers"; // Make sure WKOF is installed if (!wkof) { let response = confirm(script_name+' requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.'); if (response) { window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549'; } return; } wkof.include('Apiv2'); wkof.ready('Apiv2').then(init); function init() { // Wait until the top of the hour then update the review/lessons count let tpu = new PendingUpdater(true,45*1000); // no caching, look 45 seconds into the future to account for out of sync clocks wait_until(get_next_hour(), fetch_and_update_recurring); // Fetches the review/lessons counts, updates the dashboard, then does the same thing on top of every hour function fetch_and_update_recurring() { tpu.fetch_and_update(); wait_until(get_next_hour(), fetch_and_update_recurring); } // Also update lessons/reviews whenever page is switched to let lastVisibilityState = 'visible'; let vpu = new PendingUpdater(false, 0); // allow caching, no looking into the future document.addEventListener("visibilitychange", function() { if (document.visibilityState == 'visible' && lastVisibilityState == 'hidden') { vpu.fetch_and_update(); } lastVisibilityState = document.visibilityState; }); // Also update lessons/reviews whenever network status changes to online window.addEventListener('online', function () { vpu.fetch_and_update(); }); // Add CSS let css = `.lessons-and-reviews__reviews-button, .lessons-and-reviews__lessons-button, navigation-shortcut--reviews, navigation-shortcut--lessons { transition: background 300ms; }`; add_css(css, 'real-time-numbers-css'); // Also update lessons/reviews immediately if page was navigated to using back/forward button // must be run after css or else fade won't happen if this r###lts in an update let nav = window.performance.getEntriesByType('navigation'); if (nav.length > 0 && nav[0].type == 'back_forward') { vpu.fetch_and_update(); } } // Waits until a given time and executes the given function function wait_until(time, func) { setTimeout(func, time - Date.now()); } // Gets the time for the next hour in ms function get_next_hour() { let current_date = new Date(); return new Date(current_date.toDateString() + ' ' + (current_date.getHours()+1) + ':').getTime(); } // Adds CSS to document // Does not escape its input function add_css(css, id="") { document.getElementsByTagName('head')[0].insertAdjacentHTML('beforeend', `<style id="${id}">${css}</style>`); } // Handles fetching and displaying updates to pending lesson and review counts class PendingUpdater { // force_update (bool): when true, don't use cached data even if // age of cached data is < 60 seconds (default: false) // dt (number): # of ms to look ahead into the future when computing // what reviews/lessons are/will be available (default: 0) constructor(force_update, dt) { if (typeof(force_update) == 'undefined') force_update = false; if (typeof(dt) == 'undefined') dt = 0; this.force_update = force_update; this.dt = dt; this.thresholds = {reviews: [0,1,50,100,250,500,1000], // thresholds where reviews button image changes lessons: [0,1,25,50,100,250,500]}; // thresholds where lessons button image changes this.threshold_cls_prefix = {reviews: "lessons-and-reviews__reviews-button--", lessons: "lessons-and-reviews__lessons-button--"}; } // Fetches the review/lessons counts, updates the counts on the page fetch_and_update() { this.fetch_pending_counts() .then(this.update_pending_counts.bind(this)); } // Retreives the number of reviews/lessons due async fetch_pending_counts() { let data = await wkof.Apiv2.get_endpoint('summary', {force_update: this.force_update}); return {reviews: this.get_pending(data.reviews).length, lessons: this.get_pending(data.lessons).length}; } // Given a list of reviews/lessons returned from the api, // Returns available pending reviews/lessons as of current time + this.dt get_pending(lst) { let pending = []; let reference_time = Date.now() + this.dt; for (let i=0; i<lst.length; i++) { if (Date.parse(lst[i].available_at) <= reference_time) pending.push(...lst[i].subject_ids); } return pending; } // Update both the review and lessons counts in both title bar and big button if on the dashboard // Update the count in the top right if on the lessons / reviews summary page update_pending_counts(counts) { let url = new URL(document.URL); if (['','/','/dashboard','/dashboard/'].includes(url.pathname)) { this.dashboard_update_pending_count(counts.lessons, 'lessons'); this.dashboard_update_pending_count(counts.reviews, 'reviews'); } else if (['/review','/review/'].includes(url.pathname)) { this.summary_update_pending_count(counts.reviews, 'review'); } else if (['/lesson','/lesson/'].includes(url.pathname)) { this.summary_update_pending_count(counts.lessons, 'lesson'); } } // Update the review or lessons count in both title bar and big button for the dashboard dashboard_update_pending_count(count, reviews_or_lessons) { // update count that shows up in title bar when scrolling let reviews_elem = document.getElementsByClassName('navigation-shortcut--' + reviews_or_lessons)[0]; reviews_elem.setAttribute('data-count', count); reviews_elem.getElementsByTagName('span')[0].innerText = count; // update count in big button at top of page let big_reviews_elem = document.getElementsByClassName('lessons-and-reviews__' + reviews_or_lessons + '-button')[0]; for (let i=0; i<big_reviews_elem.classList.length; i++) { if (big_reviews_elem.classList[i].startsWith(this.threshold_cls_prefix[reviews_or_lessons])) { big_reviews_elem.classList.remove(big_reviews_elem.classList[i]); break; } } let review_threshold = Math.max( ...this.thresholds[reviews_or_lessons].filter(threshold => threshold <= count) ); big_reviews_elem.classList.add(this.threshold_cls_prefix[reviews_or_lessons] + review_threshold); big_reviews_elem.getElementsByTagName('span')[0].innerText = count; } // Update the review or lessons count in the top right of the review or lessons summary page // The second argument is singular here and plural in dashboard_update_pending_count(...). summary_update_pending_count(count, review_or_lesson) { let link = document.querySelector('#start-session a'); let cl = link.classList; if (count == 0) { link.setAttribute('title', 'No ' + review_or_lesson + 's in queue'); cl.add('disabled'); // ignores duplicates automatically $('#start-session a').on('click', (e) => e.preventDefault()); // add jQuery event handler that prevents click } else if (count > 0) { link.setAttribute('title', 'Start ' + review_or_lesson + ' session'); cl.remove('disabled'); $('#start-session a').off('click'); // remove jQuery event handler that prevents click } document.getElementById(review_or_lesson + '-queue-count').innerText = count; } } })();