Add the count of total lessons to the Today's Lessons widget
当前为
// ==UserScript==
// @name Show Total Lesson Count - WaniKani
// @namespace https://codeberg.org/lupomikti
// @version 0.6.2
// @description Add the count of total lessons to the Today's Lessons widget
// @license MIT
// @author LupoMikti
// @match https://www.wanikani.com/*
// @grant none
// @supportURL https://github.com/lupomikti/Userscripts/issues
// ==/UserScript==
// Additional supportURL: https://community.wanikani.com/t/userscript-show-total-lesson-count/66776
( async function () {
"use strict";
/* global wkof */
const scriptId = "show_total_lesson_count";
const scriptName = "Show Total Lesson Count";
const globalState = {
initialLoad: true,
stateStarting: false,
todaysLessonsFrameLoaded: false,
navBarCountFrameLoaded: false,
hasOutputLog: false,
turboEventBusy: false,
mainRetryCounter: 4,
};
let debugLogText = `START: ${scriptName} Debug Log:\n`;
let mainSource = "";
const INTERNAL_DEBUG_TURBO_HANDLING = false;
let todaysLessonsCount;
let settings;
function addToDebugLog( message ) {
debugLogText += `${new Date().toISOString()}: ${message}\n`;
}
function printDebugLog( force = false ) {
if ( !globalState.hasOutputLog || force ) {
console.log(
`${scriptName}: Outputting a debug log to console.debug()\nTo disable this setting, open "Settings > ${scriptName}" and toggle "Enable console debugging" off`,
);
console.debug( debugLogText );
}
if ( !force ) globalState.hasOutputLog = true;
debugLogText = `START: ${scriptName} Debug Log:\n`;
}
if ( !window.wkof ) {
// deno-fmt-ignore
if (confirm(`${scriptName} requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?`)) {
window.location.href =
"https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549";
}
return;
}
const wkofTurboEventsScriptUrl =
"https://update.greasyfork.org/scripts/501980/1426289/Wanikani%20Open%20Framework%20Turbo%20Events.user.js";
addToDebugLog( `Attempting to load the TurboEvents library script...` );
await wkof.load_script( wkofTurboEventsScriptUrl, /* use_cache */ true );
addToDebugLog( `Checking if TurboEvents library script is loaded in...` );
const injectedDependency = document.head.querySelector( 'script[uid*="Turbo"]' );
addToDebugLog( `Turbo Events library ${injectedDependency ? "is" : "is NOT"} loaded.` );
if ( INTERNAL_DEBUG_TURBO_HANDLING ) {
window.addEventListener( "turbo:load", () => {
console.log( `DEBUG: turbo:load has fired` );
} );
window.addEventListener( "turbo:before-frame-render", ( e ) => {
console.log( `DEBUG: turbo:before-frame-render has fired for '#${e.target.id}'` );
} );
window.addEventListener( "turbo:frame-load", ( e ) => {
console.log( `DEBUG: turbo:frame-load has fired for '#${e.target.id}'` );
} );
}
const _init = async ( source ) => {
if ( globalState.stateStarting ) {
addToDebugLog(
`SOURCE = "${source}" | We are already in the starting state, no need to initialize, returning...`,
);
return;
}
addToDebugLog( `SOURCE = "${source}" | Setting global state and calling _start()` );
globalState.initialLoad = globalState.stateStarting = true;
globalState.mainRetryCounter = 4; // ensure retries are set to 4 when initializing
globalState.hasOutputLog = globalState.todaysLessonsFrameLoaded = globalState.navBarCountFrameLoaded = false;
await _start();
};
wkof.ready( "TurboEvents" ).then( () => {
addToDebugLog( `Start of TurboEvents ready callback` );
const urlList = [
wkof.turbo.common.locations.dashboard,
wkof.turbo.common.locations.items_pages,
// vvvv Any page with the nav bar that's not one of the above locations vvvv
/^https:\/\/www\.wanikani\.com\/(settings|level|radicals|kanji|vocabulary)(\/|\?difficulty=).+\/?$/,
];
wkof.turbo.events.load.addListener( async ( _e ) => {
globalState.turboEventBusy = true;
await _init( "turbo:load" ).then( () => {
globalState.turboEventBusy = false;
} );
}, { urls: urlList, passive: true } );
wkof.turbo.events.before_frame_render.addListener( async ( e ) => {
globalState.turboEventBusy = true;
const frameId = e.target.id;
addToDebugLog( `turbo:before-frame-render has fired for "#${frameId}"` );
if ( globalState.initialLoad && !globalState.stateStarting ) {
addToDebugLog(
`globalState.initialLoad is true (no frames were previously retrieved) and we are not already in starting state, starting initialization sequence...`,
);
await _init( "turbo:before-frame-render" );
return;
}
// NOTE: with the widget system in place, this ID is never present anymore
if ( frameId === "todays-lessons-frame" ) {
globalState.todaysLessonsFrameLoaded = false;
}
// NOTE: this frame still fires on the dashboard but will never load content, it should only be checked if we are on a non-dashboard URL
else if ( frameId === "lesson-and-review-count-frame" ) {
globalState.navBarCountFrameLoaded = false;
}
}, { urls: urlList, passive: true } );
wkof.turbo.events.frame_load.addListener( async ( e ) => {
const frameId = e.target.id;
addToDebugLog(
`turbo:frame-load was fired for "#${frameId}", ${
[ "todays-lessons-frame", "lesson-and-review-count-frame" ].includes( frameId )
? "calling main function"
: "doing nothing..."
}`,
);
if ( !globalState.stateStarting ) {
addToDebugLog(
`DETOUR - turbo:frame-load was fired before we could begin starting or after main fully finished before frame events fired; changing following invocations of main() to _start() if we are not 'doing nothing'`,
);
}
mainSource = `turbo:frame-load for "#${frameId}"`;
if ( frameId === "todays-lessons-frame" ) {
globalState.todaysLessonsFrameLoaded = true;
if ( !globalState.stateStarting ) {
globalState.stateStarting = true;
await _start();
return;
}
await main();
}
else if ( frameId === "lesson-and-review-count-frame" ) {
globalState.navBarCountFrameLoaded = true;
if ( !globalState.stateStarting ) {
globalState.stateStarting = true;
await _start();
return;
}
await main();
}
mainSource = "";
globalState.turboEventBusy = false;
}, { urls: urlList, passive: true } );
addToDebugLog( `All turbo callbacks have been sent to TurboEvents library to be registered` );
} ).catch( ( err ) => {
addToDebugLog( `TurboEvents library rejected with error: ${err}` );
} ).finally( () => {
if ( INTERNAL_DEBUG_TURBO_HANDLING ) {
addToDebugLog( `SOURCE = "turbo ready finally"` );
printDebugLog( INTERNAL_DEBUG_TURBO_HANDLING );
}
_init( `wkof.ready('TurboEvents') finally callback` );
} );
async function _start() {
addToDebugLog( `Starting...` );
wkof.include( "Settings, Menu, Apiv2" );
await wkof.ready( "Settings, Menu, Apiv2" ).then( loadSettings ).then( insertMenu ).then( insertStylesheet )
.then( main )
.catch( ( err ) => {
addToDebugLog(
`wkof.ready('Settings, Menu, Apiv2') rejected (or callbacks threw an exception) with error: ${err}`,
);
} )
.finally( () => {
if ( INTERNAL_DEBUG_TURBO_HANDLING ) {
addToDebugLog( `SOURCE = "wkof modules ready finally"` );
printDebugLog( INTERNAL_DEBUG_TURBO_HANDLING );
}
} );
}
function loadSettings() {
addToDebugLog( `Loading settings...` );
const defaults = {
showTotalOnly: false,
enableDebugging: true,
};
return wkof.Settings.load( scriptId, defaults ).then( function ( wkofSettings ) {
settings = wkofSettings;
} );
}
function insertMenu() {
addToDebugLog( `Inserting menu...` );
const config = {
name: scriptId,
submenu: "Settings",
title: scriptName,
"on_click": openSettings,
};
wkof.Menu.insert_script_link( config );
mainSource = `_start() -> loadSettings() -> insertMenu()`;
}
async function saveSettings( wkofSettings ) {
globalState.hasOutputLog = false;
addToDebugLog( `Save button was clicked on settings, calling main() with new settings...` );
mainSource = "wkof.Settings.save()";
settings = wkofSettings;
await main();
mainSource = "";
}
function openSettings() {
const config = {
"script_id": scriptId,
title: scriptName,
"on_save": saveSettings,
content: {
showTotalOnly: {
type: "checkbox",
label: "Show Only Total Lesson Count",
hover_tip:
`Changes display between "<today's lesson count> / <total lesson count>" and just "<total lesson count>"`,
default: false,
},
enableDebugging: {
type: "checkbox",
label: "Enable console debugging",
hover_tip: `Enable output of debugging info to console.debug()`,
default: true,
},
},
};
const dialog = new wkof.Settings( config );
dialog.open();
}
function insertStylesheet() {
const css = `
#lesson-and-review-count-frame .lesson-and-review-count__count {
flex: 1 0 32px;
}
.todays-lessons-widget__title-container:has(.todays-lessons-widget__title-group-container) {
display: inline-flex;
gap: var(--spacing-normal);
justify-content: space-evenly;
}
.todays-lessons-widget__title-group-container {
display: flex;
flex-direction: column;
}
.todays-lessons-widget__title-group-container .todays-lessons-widget__subtitle {
margin-top: 2px;
}
.todays-lessons-widget__title-container:has(.todays-lessons-widget__title-group-container) + .todays-lessons-widget__text {
align-self: center;
}
.todays-lessons-widget__title:has(.todays-lessons-widget__text-wrapper) {
flex-direction: column;
}
.todays-lessons-widget__text-wrapper {
display: flex;
gap: var(--spacing-tight);
align-items: center;
}
`;
if ( document.getElementById( "total-lesson-count-style" ) === null ) {
const styleSheet = document.createElement( "style" );
styleSheet.id = "total-lesson-count-style";
styleSheet.textContent = css;
document.head.appendChild( styleSheet );
}
}
function getCountContainers() {
const dashboardTileCountContainer = document.querySelector(
".todays-lessons-widget__count-text .count-bubble",
);
const navBarCountContainer = document.querySelector( ".lesson-and-review-count__count" );
if ( globalState.initialLoad && ( dashboardTileCountContainer || navBarCountContainer ) ) {
const container = dashboardTileCountContainer ?? navBarCountContainer;
todaysLessonsCount = parseInt( container.textContent );
globalState.initialLoad = false;
}
return [ dashboardTileCountContainer, navBarCountContainer ];
}
async function main() {
addToDebugLog( `Main function is executing... source of start = [${mainSource}]` );
if ( !settings ) {
addToDebugLog( `We do not have settings, setting timeout on _start()` );
if ( !globalState.stateStarting ) setTimeout( _start, 50 );
else addToDebugLog( `Did not set timeout due to already being in starting state` );
addToDebugLog( `Main function is returning (source = [${mainSource}])` );
return;
}
addToDebugLog( `We have settings` );
if ( !globalState.stateStarting && globalState.hasOutputLog ) {
globalState.hasOutputLog = false;
addToDebugLog( `Main function successfully executed beforehand, preventing repeat execution...` );
if ( settings.enableDebugging ) printDebugLog( INTERNAL_DEBUG_TURBO_HANDLING );
return;
}
let lessonCountContainers;
if ( globalState.todaysLessonsFrameLoaded || globalState.navBarCountFrameLoaded ) {
addToDebugLog( `Frame(s) loaded, retrieving containers in frames containing the counts...` );
lessonCountContainers = getCountContainers();
addToDebugLog( `Count containers have been retrieved` );
}
else {
addToDebugLog( `No frames loaded, checking to see if all listened-to turbo events have settled...` );
addToDebugLog(
`starting = ${globalState.stateStarting}, turboEventBusy = ${globalState.turboEventBusy}, retries = ${globalState.mainRetryCounter}`,
);
if ( globalState.stateStarting && !globalState.turboEventBusy && globalState.mainRetryCounter > 0 ) {
addToDebugLog(
`Turbo Events have settled but we have not verified frames, using alternate verification...`,
);
let tmpContainer = document.querySelector( ".todays-lessons-widget__count-text" );
if ( tmpContainer && tmpContainer.childElementCount > 0 ) globalState.todaysLessonsFrameLoaded = true;
tmpContainer = document.getElementById( "lesson-and-review-count-frame" )?.querySelector(
".lesson-and-review-count",
);
if ( tmpContainer && tmpContainer.childElementCount > 0 ) globalState.navBarCountFrameLoaded = true;
mainSource = "main() function, no frames alternate verification";
globalState.mainRetryCounter--;
addToDebugLog(
`Alternate verification process completed, retrying main(), ${globalState.mainRetryCounter} retries left...`,
);
await main();
mainSource = "";
return;
}
else if ( globalState.mainRetryCounter === 0 ) {
addToDebugLog(
`Unable to verify loading of frames through alternate method, please refresh the page to try again from scratch`,
);
}
else {
addToDebugLog( `Turbo event callbacks are still executing, continuing...` );
}
addToDebugLog( `Main function is returning (source = [${mainSource}])` );
//if (settings.enableDebugging) printDebugLog(INTERNAL_DEBUG_TURBO_HANDLING);
return;
}
addToDebugLog( `Retrieving summary data via await of the endpoint...` );
const summaryData = await wkof.Apiv2.get_endpoint( "summary" );
addToDebugLog( `Summary data has been retrieved` );
const totalLessonCount = summaryData.lessons[0].subject_ids.length;
let isTileSizeOneThird = false;
let todaysCountForDisplay = todaysLessonsCount;
if ( lessonCountContainers.every( ( node ) => node == null ) ) {
addToDebugLog( `No nodes in containers` );
addToDebugLog( `Main function is returning (source = [${mainSource}])` );
//if (settings.enableDebugging) printDebugLog(INTERNAL_DEBUG_TURBO_HANDLING);
return;
}
addToDebugLog( `At least one container exists` );
globalState.stateStarting = false;
if ( isNaN( todaysLessonsCount ) ) todaysCountForDisplay = 0;
// must be the lesson tile container
if ( lessonCountContainers[0] ) {
const lessonCountContainer = lessonCountContainers[0];
if ( settings.showTotalOnly ) {
lessonCountContainer.textContent = totalLessonCount;
addToDebugLog(
`Setting display amount for Today's Lessons tile, set to ${lessonCountContainer.textContent}`,
);
}
else {
// The following follows no style conventions or cleanliness conventions, it's more akin to a bandaid than anything due to limited time
// If anyone would like to clean this up, please feel free to submit a PR
lessonCountContainer.textContent = todaysCountForDisplay; // in case it is zero
const titleContainer = lessonCountContainer.parentNode.parentNode.parentNode; // .todays-lessons-widget__title-container
isTileSizeOneThird = titleContainer.closest( "turbo-frame.dashboard__widget--one-third" ) != null;
if ( isTileSizeOneThird ) {
// Do the one-third stuff
const wrapper0 = document.createElement( "div" );
wrapper0.classList.add( `todays-lessons-widget__text-wrapper` );
titleContainer.querySelector( `.todays-lessons-widget__title` )?.firstElementChild
?.insertAdjacentElement( "afterend", wrapper0 );
const countTextDiv = titleContainer.querySelector( `.todays-lessons-widget__count-text` );
const countTextClone = countTextDiv.cloneNode( true );
countTextClone.firstElementChild.textContent = totalLessonCount;
const forwardSlash = `<div class="todays-lessons-widget__title-text">/</div>`;
wrapper0.insertAdjacentHTML( "afterbegin", forwardSlash );
wrapper0.prepend( countTextDiv );
wrapper0.append( countTextClone );
addToDebugLog( `Manipulated DOM and created new nodes as needed.` );
}
else {
// Do things that should only be done if two-thirds or full row
if ( !titleContainer.children[0].className.includes( `__title-group-container` ) ) {
// clone existing children, and modify clones to make new children, store in a temp array
const tempArray = [];
for ( const elem of titleContainer.children ) {
const clone = elem.cloneNode( true );
if ( clone.className.includes( `__subtitle` ) ) {
clone.textContent = "Total";
}
if ( clone.className.includes( `__title` ) ) {
const tmpNode = clone.querySelector(
`.todays-lessons-widget__count-text .count-bubble`,
);
if ( tmpNode ) tmpNode.textContent = totalLessonCount;
}
tempArray.push( clone );
}
// wrap the existing children in a new div.todays-lessons-widget__title-group-container
// wrap the new children in the same kind of wrapper
// append second wrapper after first wrapper
const wrapper1 = document.createElement( "div" );
wrapper1.classList.add( `todays-lessons-widget__title-group-container` );
titleContainer.insertAdjacentElement( "beforebegin", wrapper1 );
wrapper1.append( ...titleContainer.children );
titleContainer.prepend( wrapper1 ); // this should move the wrapper inside titleContainer
const wrapper2 = document.createElement( "div" );
wrapper2.classList.add( `todays-lessons-widget__title-group-container` );
titleContainer.insertAdjacentElement( "beforebegin", wrapper2 );
wrapper2.append( ...tempArray );
titleContainer.append( wrapper2 );
addToDebugLog( `Created additional cloned nodes to display Total Lesson Count.` );
}
}
}
}
// must be the nav bar container
if ( lessonCountContainers[1] ) {
// deno-fmt-ignore
lessonCountContainers[1].textContent = settings.showTotalOnly ?
`${totalLessonCount}` :
`${todaysCountForDisplay} / ${totalLessonCount}`;
addToDebugLog(
`Setting display amount for navigation bar, set to ${lessonCountContainers[1].textContent}`,
);
}
// hide "Today's" subtitle if showing only total OR if tile is only of size one-third
const lessonSubtitle = document.querySelector( ".todays-lessons-widget__subtitle" );
if ( ( settings.showTotalOnly || isTileSizeOneThird ) && lessonSubtitle && lessonSubtitle.checkVisibility() ) {
addToDebugLog( `Hiding the "Today's" subtitle on the lesson tile` );
lessonSubtitle.style.display = "none";
}
addToDebugLog( `Main function has successfully executed` );
if ( settings.enableDebugging ) printDebugLog( INTERNAL_DEBUG_TURBO_HANDLING );
}
} )();