// ==UserScript==
// @name WaniKani Lesson Filter
// @namespace https://www.wanikani.com
// @description Filter your lessons by type, while maintaining WaniKani's lesson order.
// @author seanblue
// @version 1.9.1
// @match https://www.wanikani.com/subjects*
// @match https://preview.wanikani.com/subjects*
// @grant none
// ==/UserScript==
(async function(Turbo, wkof) {
'use strict';
var wkofMinimumVersion = '1.1.0';
if (!wkof) {
var response = confirm('WaniKani Lesson Filter 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;
}
if (!wkof.version || wkof.version.compare_to(wkofMinimumVersion) === 'older') {
alert(`WaniKani Lesson Filter requires at least version ${wkofMinimumVersion} of WaniKani Open Framework.`);
return;
}
const localStorageSettingsKey = 'lessonFilter_inputData';
const localStorageSettingsVersion = 2;
const radicalSubjectType = 'radical';
const kanjiSubjectType = 'kanji';
const vocabSubjectType = 'vocabulary';
const batchSizeInputSelector = '#lf-batch-size';
const radicalInputSelector = '#lf-radicals';
const kanjiInputSelector = '#lf-kanji';
const vocabInputSelector = '#lf-vocab';
const style =
'';
const html =
'
' +
'
Items to Learn
' +
'
' +
'
' +
'' +
'' +
'
' +
'
';
let queueInitializedPromise;
let initialLessonQueue;
let initialBatchSize;
let currentLessonQueue;
let currentBatchSize;
async function initialize() {
queueInitializedPromise = initializeLessonQueue();
await queueInitializedPromise;
}
async function initializeLessonQueue() {
wkof.include('Apiv2');
await wkof.ready('Apiv2');
let [ unsortedLessonQueue, userPreferences ] = await Promise.all([getUnsortedLessonQueue(), getUserPreferences()]);
initialBatchSize = userPreferences.batchSize;
initialLessonQueue = sortInitialLessonQueue(unsortedLessonQueue, userPreferences.lessonOrder);
currentLessonQueue = [...initialLessonQueue];
currentBatchSize = initialBatchSize;
console.log(currentLessonQueue);
return Promise.resolve('done');
}
async function getUnsortedLessonQueue() {
let summary = await wkof.Apiv2.fetch_endpoint('summary');
let lessonIds = summary.data.lessons.flatMap(l => l.subject_ids);
let lessonData = await wkof.Apiv2.fetch_endpoint('subjects', { filters: { ids: lessonIds } });
return lessonData.data.map(d => ({ id: d.id, level: d.data.level, subjectType: d.object, lessonPosition: d.data.lesson_position }));
}
async function getUserPreferences() {
let response = await wkof.Apiv2.fetch_endpoint('user');
return {
batchSize: response.data.preferences.lessons_batch_size,
lessonOrder: response.data.preferences.lessons_presentation_order
};
}
function sortInitialLessonQueue(queue, lessonOrder) {
let typeOrder = [radicalSubjectType, kanjiSubjectType, vocabSubjectType];
if (lessonOrder === 'ascending_level_then_subject') {
return queue.sort((a, b) => a.level - b.level || typeOrder.indexOf(a.subjectType) - typeOrder.indexOf(b.subjectType) || a.lessonPosition - b.lessonPosition);
}
shuffle(queue);
if (lessonOrder === 'ascending_level_then_shuffled') {
queue.sort((a, b) => a.level - b.level);
}
return queue;
}
async function setupUI() {
if (!onLessonPage()) {
return;
}
wkof.include('Jquery');
await wkof.ready('Jquery');
console.log($);
//$('#batch-items').addClass('lf-nofixed');
$('head').append(style);
$('.subject-queue').before(html);
//loadSavedInputData();
}
function loadSavedInputData() {
let savedDataString = localStorage[localStorageSettingsKey];
if (!savedDataString) {
return;
}
let savedData = JSON.parse(savedDataString);
if (savedData.version !== localStorageSettingsVersion) {
delete localStorage[localStorageSettingsKey];
return;
}
let data = savedData.data;
$(batchSizeInputSelector).val(data.batchSize);
$(radicalInputSelector).val(data.radicals);
$(kanjiInputSelector).val(data.kanji);
$(vocabInputSelector).val(data.vocab);
}
function setupEvents() {
$('#lf-apply-filter').on('click', applyFilter);
$('#lf-apply-shuffle').on('click', applyShuffle);
$('#lf-main').on('keydown, keypress, keyup', '.lf-input', disableWaniKaniKeyCommands);
}
function applyFilter(e) {
let rawFilterValues = getRawFilterValuesFromUI();
filterLessonsInternal(rawFilterValues);
saveRawFilterValues(rawFilterValues);
$(e.target).blur();
}
async function filterLessonsInternal(rawFilterValues) {
await queueInitializedPromise;
let newFilteredQueue = getFilteredQueue(rawFilterValues);
if (newFilteredQueue.length === 0) {
alert('You cannot remove all lessons');
return;
}
currentLessonQueue = newFilteredQueue;
let newBatchedSize = getCheckedBatchSize(rawFilterValues.batchSize);
currentBatchSize = newBatchedSize;
console.log(newFilteredQueue);
console.log(newBatchedSize);
visitUrlForCurrentBatch();
}
function getRawFilterValuesFromUI() {
return {
'batchSize': $(batchSizeInputSelector).val(),
'radicals': $(radicalInputSelector).val(),
'kanji': $(kanjiInputSelector).val(),
'vocab': $(vocabInputSelector).val()
};
}
function getFilteredQueue(rawFilterValues) {
let idToIndex = { };
for (let i = 0; i < currentLessonQueue.length; i++) {
idToIndex[currentLessonQueue[i].id] = i;
}
let filteredRadicalQueue = getFilteredQueueForType(radicalSubjectType, rawFilterValues.radicals);
let filteredKanjiQueue = getFilteredQueueForType(kanjiSubjectType, rawFilterValues.kanji);
let filteredVocabQueue = getFilteredQueueForType(vocabSubjectType, rawFilterValues.vocab);
return filteredRadicalQueue.concat(filteredKanjiQueue).concat(filteredVocabQueue).sort((a, b) => idToIndex[a.id] - idToIndex[b.id]);
}
function getFilteredQueueForType(subjectType, rawFilterValue) {
let filterValue = parseInt(rawFilterValue);
if (filterValue <= 0) {
return [];
}
let queueForType = getQueueForType(subjectType);
if (isNaN(filterValue)) {
return queueForType;
}
return queueForType.slice(0, filterValue);
}
function getQueueForType(subjectType) {
return currentLessonQueue.filter(item => item.subjectType === subjectType);
}
function getCheckedBatchSize(rawValue) {
let value = parseInt(rawValue);
if (isNaN(value)) {
return currentBatchSize;
}
if (value < 0) {
return 0;
}
return value;
}
function applyShuffle(e) {
shuffleLessonsInternal();
$(e.target).blur();
}
async function shuffleLessonsInternal() {
await queueInitializedPromise;
shuffle(currentLessonQueue);
visitUrlForCurrentBatch();
}
function shuffle(array) {
// https://stackoverflow.com/a/12646864
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
async function resetInternal() {
await queueInitializedPromise;
currentLessonQueue = initialLessonQueue;
currentBatchSize = initialBatchSize;
visitUrlForCurrentBatch();
}
function visitUrlForCurrentBatch() {
if (currentLessonQueue.length === 0) {
Turbo.visit(`/dashboard`);
}
let lessonBatchQueryParam = getCurrentLessonBatchIds().join('-');
Turbo.visit(`/subjects/${currentLessonQueue[0].id}/lesson?queue=${lessonBatchQueryParam}`);
}
function getCurrentLessonBatchIds() {
return currentLessonQueue.slice(0, currentBatchSize).map(item => item.id);
}
function saveRawFilterValues(rawFilterValues) {
let settings = {
'version': localStorageSettingsVersion,
'data': rawFilterValues
};
localStorage[localStorageSettingsKey] = JSON.stringify(settings);
}
function disableWaniKaniKeyCommands(e) {
e.stopPropagation();
}
function enableInputs(e) {
$(e.currentTarget).prop('disabled', false);
}
function isNewBatchUrl(url) {
return new URL(url).pathname === '/subjects/lesson';
}
function setsAreEqual(set1, set2) {
return set1.size === set2.size && [...set1].every(v => set2.has(v));
}
window.addEventListener("turbo:before-visit", function(e) {
if (isNewBatchUrl(e.detail.url)) {
e.preventDefault();
let currentLessonBatchIdSet = new Set(getCurrentLessonBatchIds());
initialLessonQueue = initialLessonQueue.filter(item => !currentLessonBatchIdSet.has(item.id));
currentLessonQueue = currentLessonQueue.filter(item => !currentLessonBatchIdSet.has(item.id));
visitUrlForCurrentBatch();
}
});
window.lessonFilter = {
shuffle: () => {
shuffleLessonsInternal()
},
filter: (radicalCount, kanjiCount, vocabCount, batchSize) => {
let rawFilterValues = {
'radicals': radicalCount,
'kanji': kanjiCount,
'vocab': vocabCount,
'batchSize': batchSize
};
filterLessonsInternal(rawFilterValues);
},
reset: () => {
resetInternal();
}
}
await initialize();
})(window.Turbo, window.wkof);