What is it
Automatically updates the review and lesson counts everywhere on the site. Not sure why WK doesn’t do this on its own. No more refreshing to see if you have new reviews!
Where
Available at: →The Macaron Palace←
Automatically updates the review and lesson counts everywhere on the site. Not sure why WK doesn’t do this on its own. No more refreshing to see if you have new reviews!
Available at: →The Macaron Palace←
Thanks for making that script, it’s more attention-grabbing to have the grey 0 turn purple. Hopefully it works in 25 minutes.
Have you thought about making one that places the same thing on the top of the community site as well?
Oh, you know what, I actually forgot about the color. I think I have to change too, and not just update the count. Will add that tomorrow if I can’t find time today.
Have you seen this script for the forums? Or do you mean a script to update this count, too?
That’s exactly what I wanted, thank you. Auto-updating that count could also be useful, but if people are browsing the forum, the count is refreshing, whereas the dashboard doesn’t feel like a page you need to reload because it updates the (less visually grabbing) Next Review time.
Ok, I finished my reviews so I could tell how WK changes the color, and updated the script to that it should also change the color.
Does this script still work with the new design to automatically refresh the review count at the beginning of each hour?
Should do, yes!
It was giving me an error about the WKOF Apiv2 submodule not being found. I added an include statement for it. I also made it update the big button at the top of the page that was added in the redesign.
@@ -11,7 +11,12 @@
// @grant none
// ==/UserScript==
+wkof.include('Apiv2');
+wkof.ready('Apiv2').then(
(function() {
+ let review_thresholds = [0,1,50,100,250,500,1000]; // thresholds where reviews button image changes, in increasing order
+ let review_threshold_cls_prefix = "lessons-and-reviews__reviews-button--";
+
// Wait until the top of the hour then update the review count
wait_until(get_next_hour(), fetch_and_update);
@@ -46,11 +51,24 @@
return promise;
}
- // Update the review count on the dashboard
+ // Update the review count in both places on the dashboard
function update_review_count(review_count) {
+ // update reviews # that shows up in title bar when scrolling
var reviews_elem = document.getElementsByClassName('navigation-shortcut--reviews')[0];
reviews_elem.setAttribute('data-count', review_count);
reviews_elem.getElementsByTagName('span')[0].innerText = review_count;
+
+ // update reviews number in big button at top of page
+ var big_reviews_elem = document.getElementsByClassName('lessons-and-reviews__reviews-button')[0];
+ for (let i=0; i<big_reviews_elem.classList.length; i++) {
+ if (big_reviews_elem.classList[i].startsWith(review_threshold_cls_prefix)) {
+ big_reviews_elem.classList.remove(big_reviews_elem.classList[i]);
+ break;
+ }
+ }
+ var review_threshold = review_thresholds.filter((threshold) => threshold <= review_count).splice(-1)[0];
+ big_reviews_elem.classList.add(review_threshold_cls_prefix + review_threshold);
+ big_reviews_elem.getElementsByTagName('span')[0].innerText = review_count;
}
// Create a new promise and resolve function
@@ -58,4 +76,4 @@
var resolve, promise = new Promise((res, rej)=>{resolve = res;});
return [promise, resolve];
}
-})();
\ No newline at end of file
+}));
\ No newline at end of file
Ah, oops. Must have forgotten about that.
Oh, I thought I had already updated this. Sorry. I will incorporate all of these changes tomorrow and publish an official update
I’ve added some additional features too, if you’re interested in upstreaming any of them:
Here is the script with those additional changes. I would have provided a diff, but things moved around too much for the diff to be useful.
// ==UserScript==
// @name Wanikani: Real (Time) Numbers
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @description Updates the review count automatically as soon as new reviews are due
// @author Kumirei
// @match https://www.wanikani.com
// @match https://www.wanikani.com/dashboard
// @match https://preview.wanikani.com
// @match https://preview.wanikani.com/dashboard
// @grant none
// ==/UserScript==
wkof.include('Apiv2');
wkof.ready('Apiv2').then(
(function() {
// 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);
}
// 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() {
var current_date = new Date();
return new Date(current_date.toDateString() + ' ' + (current_date.getHours()+1) + ':').getTime();
}
// 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();
});
}));
// 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 dashboard
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() {
var 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) {
var pending = [];
var 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
update_pending_counts(counts) {
this.update_pending_count(counts.lessons, 'lessons');
this.update_pending_count(counts.reviews, 'reviews');
}
// Update the review or lessons count in both title bar and big button
update_pending_count(count, reviews_or_lessons) {
// update count that shows up in title bar when scrolling
var 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
var 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;
}
}
var 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;
}
}
// function for adding style
// from https://greasyfork.org/en/scripts/35383-gm-addstyle-polyfill/code
GM_addStyle = function (aCss) {
'use strict';
let head = document.getElementsByTagName('head')[0];
if (head) {
let style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.textContent = aCss;
head.appendChild(style);
return style;
}
return null;
};
// Fade backgrounds on lessons/reviews update
GM_addStyle(`
.lessons-and-reviews__reviews-button, .lessons-and-reviews__lessons-button,
navigation-shortcut--reviews, navigation-shortcut--lessons {
transition: background 300ms;
}
`);
Is there any particular reason as to why you put the class and CSS functions in the global scope?
There’s no good reason to make it global. Below is a version which keeps everything local and also makes the script work for the count in the button in the top right corner of the lesson/review summary page.
// ==UserScript==
// @name Wanikani: Real (Time) Numbers
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @description Updates the review count automatically as soon as new reviews are due
// @author Kumirei
// @match https://www.wanikani.com
// @match https://www.wanikani.com/dashboard
// @match https://www.wanikani.com/review
// @match https://www.wanikani.com/lesson
// @match https://preview.wanikani.com
// @match https://preview.wanikani.com/dashboard
// @match https://preview.wanikani.com/review
// @match https://preview.wanikani.com/lesson
// @grant none
// ==/UserScript==
(function() {
wkof.include('Apiv2');
wkof.ready('Apiv2').then(initialize)
function initialize() {
// 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);
}
// 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() {
var current_date = new Date();
return new Date(current_date.toDateString() + ' ' + (current_date.getHours()+1) + ':').getTime();
}
// 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();
});
}
// 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--"};
this.session_start_tooltip_with_pending = {review: 'Start review session',
lesson: "Start lessons"};
}
// 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() {
var 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) {
var pending = [];
var 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) {
var 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
var 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
var 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;
}
}
var 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) {
var link = document.querySelector('#start-session a')
var cl = link.classList;
if (count == 0) {
link.setAttribute('title', 'No ' + review_or_lesson + 's in queue');
cl.add('disabled'); // ignores duplicates automatically
} else if (count > 0) {
link.setAttribute('title', this.session_start_tooltip_with_pending[review_or_lesson]);
cl.remove('disabled');
}
document.getElementById(review_or_lesson + '-queue-count').innerText = count;
}
}
// function for adding style
// from https://greasyfork.org/en/scripts/35383-gm-addstyle-polyfill/code
GM_addStyle = function (aCss) {
'use strict';
let head = document.getElementsByTagName('head')[0];
if (head) {
let style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.textContent = aCss;
head.appendChild(style);
return style;
}
return null;
};
// Fade backgrounds on lessons/reviews update for dashboard.
// Summary pages don't need special style rule
// because they already have a similar one.
GM_addStyle(`
.lessons-and-reviews__reviews-button, .lessons-and-reviews__lessons-button,
navigation-shortcut--reviews, navigation-shortcut--lessons {
transition: background 300ms;
}
`);
})();
I already updated the script with your previous additions (in a local scope), but I’ll add the summary page function
Thanks! Below is a diff with a few bug fixes to the 1.2.0 version:
The @match
list in the version you uploaded didn’t include the urls for the review / summary pages. Also, it seems that the rules also need to include the quiz session pages or else the script doesn’t load on the summary page if it was navigated to automatically at the end of a quiz session.
WaniKani has a jQuery click event listener which prevented clicking the “start session” button on the summary pages after the count is updated from zero to nonzero.
Calls to fetch_and_update_recurring were failing because the variable tpu was out of scope.
Navigating away from a page and then using the back/forward button caused the page to forget any updated counts. The script now triggers an immediate update if the page was navigated to using the back/forward button.
The title attributes applied were updated to match the ones WaniKani uses.
The diff below fixes all five issues. Thanks again!
@@ -1,17 +1,28 @@
// ==UserScript==
// @name Wanikani: Real (Time) Numbers
// @namespace http://tampermonkey.net/
-// @version 1.2.0
+// @version 1.2.1
// @description Updates the review count automatically as soon as new reviews are due
// @author Kumirei
// @match https://www.wanikani.com
// @match https://www.wanikani.com/dashboard
+// @match https://www.wanikani.com/review
+// @match https://www.wanikani.com/review/*
+// @match https://www.wanikani.com/lesson
+// @match https://www.wanikani.com/lesson/*
// @match https://preview.wanikani.com
// @match https://preview.wanikani.com/dashboard
+// @match https://preview.wanikani.com/review
+// @match https://preview.wanikani.com/review/*
+// @match https://preview.wanikani.com/lesson
+// @match https://preview.wanikani.com/lesson/*
// @grant none
// ==/UserScript==
/*jshint esversion: 8 */
+// @match 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
@@ -30,6 +41,12 @@
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
@@ -51,12 +68,13 @@
transition: background 300ms;
}`;
add_css(css, 'real-time-numbers-css');
- }
- // 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 immediately if page was navigated to using back/forward button
+ // must be run after css or else fade won't happen if this results 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
@@ -70,6 +88,8 @@
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>`);
}
@@ -91,8 +111,6 @@
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--"};
- this.session_start_tooltip_with_pending = {review: 'Start review session',
- lesson: "Start lessons"};
}
// Fetches the review/lessons counts, updates the counts on the page
@@ -164,9 +182,11 @@
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', this.session_start_tooltip_with_pending[review_or_lesson]);
+ 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;
}
The script is more you than it is me now, haha. Might have been easier for you to just create a new script
Anyway, I have published the fixes. Only change I made was that I consolidated all the @match
es into a single @include /^https://(www|preview).wanikani.com/(lesson/*|review/*|dashboard)?/