[For Userscript Authors] WK Queue Manipulator

What Is It?

Queue Manipulator is a user-created library script that can be used by other userscripts. It simplifies and coordinates the manipulation of the review/lesson queue.

How Can I Use It?

Queue Manipulator can be required in your userscript:

// @require      https://greasyfork.org/scripts/462049-wanikani-queue-manipulator/code/WaniKani%20Queue%20Manipulator.user.js?version=1386112

Alternatively, the end user can be instructed to install Queue Manipulator manually – similar to Open Framework. The @require method is more comfortable for the end user, but comes with a few disadvantages:

  • The @require meta data has to be kept up-to-date manually (I don’t plan to do many updates for Queue Manipulator, but they might be necessary every time WK changes the way it handles the queue.
  • The entire code of Queue Manipulator is inserted into every script that @requires it, which can increase the parsing time by a few milliseconds.

If Queue Manipulator is available (by @require or by manually installing it), you can use it via the wkQueue object in the global scope. For example:

window.wkQueue.addReorder(q => q.reverse());

The lesson manipulation functionality requires Open Framework. If only reviews are manipulated, Open Framework is not needed.

Documentation

Selectors

This library script can be used similarly to Item Info Injector, but is much simpler since there is only one selector: on(). This selector can be used to specify on which pages your registered manipulation should be applied:

wkQueue.on("lesson, review, extraStudy").addFilter(q => q.slice(0, 5));

The available keywords are lesson, lessonQuiz, review, and extraStudy. Omitting the selector is the same as using all four keywords.

Actions

The selector chain determines when something should happen – the “action” that is appended to the end of the chain determines what should happen. The available actions are:

Action Description
addTotalChange() Intended for manipulations that add/remove/reorder subjects in the queue.
addFilter() Intended for manipulations that remove/reorder subjects in the queue.
addReorder() Intended for manipulations that reorder subjects in the queue.
addPostprocessing() Can be used to change the subject data that will be written into the id="quiz-queue" DOM element.

All these functions take two arguments: a \textcolor{green}{\text{callback}} function that applies the manipulation, and an optional \textcolor{orange}{\text{settings}} object. The \textcolor{green}{\text{callback}} function will be called when needed and receives two arguments:

  • an array (which is the queue)
  • an object with information about the current state

The current state object only has one property: on, which can be either "lesson", "lessonQuiz", "review", or "extraStudy". Each element of the array is an object with the following properties:

Property Description
id The WaniKani subject id of the item.
srs The SRS level of the item. Only available on the review page, and if the item was not added by a manipulation.
subject The subject as it is structured in the id="quiz-queue" DOM element. Only guaranteed to be available if the \textcolor{orange}{\text{settings}} object passed to the action contained {subject: true}.
item The Open Framework item as it was returned by Open Framework. Only guaranteed to be available if the \textcolor{orange}{\text{settings}} object passed to the action contained {openFramework: true}. Use openFrameworkGetItemsConfig to specify any further endpoints you need, e.g.

Return Value

Registering an action returns an object containing a remove() function. Calling this function unregisters the manipulation and causes the queue to be recomputed.

Other Functionality

Settings

Aside from registering manipulations, wkQueue also offers a way to change two review settings provided by WaniKani: completeSubjectsInOrder and questionOrder. Setting completeSubjectsInOrder to true results in the same behavior as in extra study sessions: for every subject, both reading and meaning question has to be answered correctly before proceeding to the next subject. questionOrder can be set to either meaningFirst or readingFirst.

For lessons, lessonBatchSize can be used to change the batch size. To go back to the default batch size, set it to null.

wkQueue.completeSubjectsInOrder = true;
wkQueue.questionOrder = "meaningFirst";
wkQueue.lessonBatchSize = 15;

applyManipulation()

This can be used to apply a manipulation to the current queue without causing it to be recomputed and without registering the manipulation. This means that it will only be applied once. I initially created this function with the intention to use it in my Later Crabigator userscript to push the current element to the end of the queue – it would only be a change to the current queue, and if the queue gets reordered, it does not matter that the change is lost. However, since every queue manipulation has the side effect of WaniKani forgetting all half-answered subjects, I did not end up using this function at all.

refresh()

Use this to cause the queue to be recomputed.

wkQueue.refresh();

currentLessonQueue(), currentReviewQueue()

Use these functions to retrieve the current queues. Takes the same \textcolor{orange}{\text{settings}} object as described above as an optional argument. Returns the same queue array as passed to the \textcolor{green}{\text{callback}} described before.

Example Code

Only keep kanji:

let kanjiFilter = wkQueue.addFilter(q => q.filter(i => i.subject.subject_category === `Kanji`), {subject: true});
// to remove the filter: kanjiFilter.remove();

Order by level:

let levelSort = wkQueue.on(`review,extraStudy`).addReorder(q => q.sort((i0, i1) => i0.item.data.level - i1.item.data.level), {openFramework: true});

Order by SRS level:

wkQueue.addReorder(orderBySrs, {openFramework: true, openFrameworkGetItemsConfig: "assignments"});

function orderBySrs(currentQueue) {
    return currentQueue.sort((i0, i1) => i0.item.assignments.srs_stage - i1.item.assignments.srs_stage);
}

Completely replace the queue with a new queue containing the items with id 1, 2, and 3:

wkQueue.addTotalChange(() => [1, 2, 3]);

Add “こういち” as accepted reading to all vocabulary items:

wkQueue.addPostprocessing(q => q.forEach(i => {
	if (i.subject.subject_category === "Vocabulary") {
		i.subject.readings.push({reading: "こういち", pronunciation: {sources: []}});
	}
}));

Warnings

Every time the queue is manipulated, WaniKani forgets about half-finished items. It is recommended to only register manipulations at script start and not cause any queue recomputations halfway through a review session.

Furthermore, do not store any wkQueue properties for later access: The global wkQueue object might get replaced by a newer version at any time – for example, if script A @requires version 1.0, but later, Tampermonkey injects script B which @requires version 1.2, the version 1.0 wkQueue will get replaced (and any already registered manipulations get transferred to the version 1.2 wkQueue). Storing a reference like const queue = window.wkQueue should be possible since all properties of wkQueue will redirect to the new version, but please do not try this:

let myRefreshReference = window.wkQueue.refresh;
// and somewhere else
myRefreshReference();

And one more problem I noticed with users of Item Info Injector and Queue Manipulator: if you choose to @require the library script in your script, you should include the version number at the end of the URL, and keep it up-to-date. The default setting for Tampermonkey is to never look for updates of @required scripts, so it will only download the current version at the time the end user installs your script, but won’t be kept up-to-date.

Feedback and Requests

If you need additional features, ask here in this thread and I will think about adding them. Also let me know if any part of the documentation is unclear, or if the script shows some unexpected behavior. Lastly, I’m not a native English speaker, so my wording might be awkward or grammatically incorrect – feel free to also correct me on that.

8 Likes

This can only be used to alter the first 100 items, right?

1 Like

I have not tested yet if the WaniKani code accepts it, but I try to store all subjects into the data-quiz-queue-target="subjects" DOM element, not just the first 100.

1 Like

I just did 110 reviews in extra study (actually self study) and it worked past 100! I’m going to be able to revive Random Voice Actor with this!

4 Likes

Random Voice Actor is back! What I do is I just set all (both) actors’ audio to be the same, so that regardless of what your default VA is it plays the same audio. This is only possible thanks to @Sinyaven’s work on the queue manipulator

3 Likes

@Sinyaven There’s an issue where Queue Manipulator is applying manipulations after the lesson quiz is completed, which overwrites my Lesson Filter queue state. It does this even when no additional manipulations were added or removed and refresh was not called. I thought you said it should only reapply manipulations when one of those things happened, but if I misunderstood let me know. See Updates to Lessons, Reviews, and Extra Study - #301 by seanblue for a way to reproduce.

@Kumirei Just to share further information, the Queue Manipulator code does only refresh the page if there’s at least one manipulation. It looks like the addTotalChange from Omega is the manipulation present that is causing this to execute. Is there a reason this has can’t be scoped to the non-lesson page when the lesson is set to “None” by using the on function? I don’t know if @Sinyaven considers the above a bug or not, but unless one of you makes a change here I don’t think there’s a way to make Omega and Lesson Filter compatible. (As previously mentioned in the other thread, even if I was also using Queue Manipulator, the reevaluation of the queue after finishing the lesson quiz is inherently incompatible with how Lesson Filter currently works. While I could “make it work” by changing the behavior, this would go counter to the polling I did which indicates that most of my users like the current way Lesson Filter manages the queue.) It’s fine if neither of you wants to make a change of course. If that’s the case I’ll just note in the Lesson Filter topic that its incompatible with Reorder Omega.

This just stopped working for me. This is for regular reviews (not lessons or extra study).

It updates the queue size count at the top right but it doesn’t seem to affect the queue now. The queue size then goes into negative numbers once you go past zero.

I’m using it like this:

    wkQueue.applyManipulation(function (queue) {
        ...
        queue = queue.filter(item => ...);
        ...
        return queue;
    });
1 Like

When you say that it “stopped working”, do you mean that it has worked before? Which browser are you using? Do you get any errors? Do you use any other scripts? Can you look at the two child elements of document.getElementById("quiz-queue") and check if the queue was correctly set?

It was a bit weird. I was using an unversioned link until today and things worked fine up until it started not actually filtering the items. Things worked correctly again when I changed to a versioned link (current version).

Thinking about it, I think it must’ve happened at the same time I switched from Gart’s fix for the Double Check script to @rfindley’s update earlier today.

WKOF 1.1.0, Double Check and my Lock script were the only scripts running.

(Edit: I thought I deleted my earlier post today! :sweat_smile:)

1 Like

I meant that the queue is not recomputed unless manipulations are added/removed or refresh is called. Queue Manipulator computes the queue at the start of the first batch and sets the URL accordingly. Then, after every finished lesson quiz, it has to set the URL for the next batch according to the initially computed queue – so it does not recompute the queue, but it does change the URL, which conflicts with your script. I don’t think there is anything I can do on my end, except maybe allowing callback functions to return null and treat it as “nevermind, I don’t want to manipulate after all”, and if every manipulation returns null, I could probably cancel the reordering. Maybe I will add this in the future – I have to think about it.

Ah, I see. I understand what you mean now.

I don’t have enough context to know if that would even solve the conflict. After all, Kumi is setting a manipulation in Omega even when it’s set to “None”. So presumably it would always reapply that manipulation and conflict with my script even with the feature you describe.

If I did use Queue Manipulator then in theory just updating the URL wouldn’t conflict since that URL update would already include my filter. But then if someone applies a new filter or shuffle after a batch was completed (which I allow) it would refetch the queue and start from scratch. Right now Lesson Filter continues to apply new filters on the already filtered list, not from scratch each time. Though to be honest, I’m not sure how much people rely on that behavior. Maybe I’ll do a poll and see if people do that at all.

One more related question on this. You previously mentioned that I could use applyManipulation to do a one-time manipulation. Let’s say I use addFilter for filtering but applyManipulation for shuffling, which I think is probably what I want. If I understand correctly, here’s what I expect to happen:

  1. The user does a filter (addFilter) and a shuffle (applyManipulation).
  2. After the user completes a lesson quiz and opts to do more lessons the queue will not change again. That is, since no additional manipulations were made, the already filtered and shuffled queue (as computed in step one) remains in use.
  3. The user decides to do another filter (I remove the old filter and add the new one via addFilter). At this point, the queue is recomputed using the new filter and not the previous shuffle since that was only done via applyManipulation. Is that correct?
  4. Additionally, if someone did another manipulation from another script (e.g. a sort via Omega), that manipulation as well as the filter from Lesson Filter would apply since they were both added, but the shuffle would not. Is that correct?

I think shuffle is a cool feature, but applying it automatically when the queue is recomputed feels silly, which is why I’m thinking this way would make the most sense if I decide to switch over to improve compatibility.

1 Like

Oh and just a small optimization you may want to make. It might be a good idea to support the fact that you can go directly to any set of lessons in the URL. That is, if the page is loaded with a specific set of lessons, tweak the fetched queue to force those lessons to the front of the queue to match what the user intended / allow a page refresh. (Not sure if you’d want to validate that they are in the list of lessons from summary or not.) Then you can skip updating the URL in cases were the registered manipulations don’t change the initial batch contents. For example, right now just having Omega installed forces a URL update because it does an addTotalChange, which forces the page back to the default order instead of what was in the URL. If you make the change I propose, this can be avoided.

1 Like

I think all four bullet points are correct – at least that is how it is intended to work. And I will think about adding your suggested change of taking the URL parameters into consideration.

1 Like

The tricky part about this is that the “None” preset is actually a preset just like any other. There’s nothing stopping a user from editing it or even removing it.

Seems like a dedicated “No Action” dropdown option (not a preset) where Omega truly does nothing might be beneficial. This isn’t just for comparability with my script. Right now the lesson page loads and then reloads with the same content because “None” is a filter. Yes, I asked @Sinyaven to update Queue Manipulator to not do a Turbo.visit if the Lesson batch hasn’t actually changed, but it could be handled from Omega as well by having a true “No Action” option. Or maybe @Sinyaven’s idea of allowing a manipulation to return null would be sufficient, if you could reasonably utilize that when a preset specifies no changes.

(Granted I don’t know how Omega works, so maybe you always need to replace the queue for reasons I’m not aware of. If so just let me know.)

1 Like

I don’t know if this is just temporary or here to stay, but currently the Stimulus controller variables are not private anymore.

I decided to update Queue Manipulator so that (during reviews) it first tries to directly access the quiz-queue controller’s variables, and if that doesn’t work, it falls back to the old method of causing the recreation of the controller. As long as the new method works, it should cause half-answered items to stay in memory, and the wkQueue.applyManipulation() callback will get the up-to-date queue as it was intended from the beginning.

// @require      https://greasyfork.org/scripts/462049-wanikani-queue-manipulator/code/WaniKani%20Queue%20Manipulator.user.js?version=1171957

I really hope they don’t change it back :worried:

1 Like

Version 1.4:

Bugfix for forwarding settings (like questionOrder) to newer version of wkQueue

// @require      https://greasyfork.org/scripts/462049-wanikani-queue-manipulator/code/WaniKani%20Queue%20Manipulator.user.js?version=1172719
1 Like

Just want to double check: applying post-processing would also interfere with @seanblue’s Lesson Filter script, right?

I’m thinking that I am probably not going to pursue compatibility by not applying total-change for “None” presets in Omega, because I also have the other features (in particular voice actor randomization) which relies on post processing.

1 Like

Anything that affects the lesson queue will probably interfere – post-processing, however, only applies to review/quiz queues.

I’m still thinking about adding the possibility to return null from the callback function to let Queue Manipulator know that nothing should be done, but I currently don’t see this as a high priority, so I’m not sure when/if I will implement it.

1 Like

@Sinyaven This doesn’t seem to be working at all for Recent Mistakes

1 Like