// ==UserScript==
// @name WK Custom SRS
// @namespace leohumnew.wk
// @version 0.3.7
// @description Add custom word packs to WaniKani!
// @author leohumnew
// @match https://www.wanikani.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=wanikani.com
// @grant GM.setValue
// @grant GM.getValue
// @grant GM_xmlhttpRequest
// @run-at document-start
// @require https://gf.qytechs.cn/scripts/489759-wk-custom-icons/code/CustomIcons.js?version=1344498
// ==/UserScript==
(async() => {
// ------------------------ Define and create custom HTML structures ------------------------
const srsNames = ["Lesson", "Apprentice 1", "Apprentice 2", "Apprentice 3", "Apprentice 4", "Guru 1", "Guru 2", "Master", "Enlightened", "Burned"];
// --------- Main popup ---------
let overviewPopup = document.createElement("dialog");
overviewPopup.id = "overview-popup";
let overviewPopupStyle = document.createElement("style");
// Styles copied in from styles.css
overviewPopupStyle.innerHTML = /*css*/ `
/* General styling */
.content-box {
background-color: var(--color-wk-panel-background);
border-radius: 3px;
padding: 1rem;
}
/* Main popup styling */
#overview-popup {
background-color: var(--color-menu, white);
width: 60%;
max-width: 50rem;
height: 60%;
max-height: 40rem;
border: none;
border-radius: 3px;
box-shadow: 0 0 1rem rgb(0 0 0 / .5);
&:focus-visible {
border: none }
p {
margin: 0 }
input, select {
margin-bottom: 0.5rem }
button {
cursor: pointer;
background-color: transparent;
border: none;
&[type="submit"], &.outline-button {
border: 1px solid var(--color-text);
border-radius: 5px;
padding: 0.2rem 0.8rem;
}
}
button:hover {
color: var(--color-tertiary, #a5a5a5);
&[type="submit"], &.outline-button {
border-color: var(--color-tertiary, #a5a5a5) }
}
> header {
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--color-tertiary, --color-text);
margin-bottom: 1rem;
> h1 {
font-size: x-large;
color: var(--color-tertiary, --color-text);
}
> button {
border: none;
color: var(--color-tertiary, --color-text);
font-size: x-large;
&:hover {
color: var(--color-text) }
&:focus-visible {
outline: none }
}
}
&::backdrop {
background-color: rgba(0, 0, 0, 0.5) }
}
/* Styling for top tabs */
#tabs {
display: flex;
flex-wrap: wrap;
> input {
display: none }
> label {
cursor: pointer;
padding: 0.5rem 1rem;
max-width: 20%;
}
> div {
display: none;
padding: 1rem;
order: 1;
width: 100%;
}
> input:checked + label {
color: var(--color-tertiary, --color-text);
border-bottom: 2px solid var(--color-tertiary, gray);
}
> input:checked + label + div {
display: initial }
}
/* Styling for the overview tab */
#tab-1__content > div {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 1.5rem;
text-align: center;
p {
font-size: xx-large }
}
/* Styling for packs in the packs tab */
#tabs .pack {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
span {
font-style: italic }
> div {
margin: auto 0 }
button {
margin-left: 10px }
> div > span, & > div > input {
margin: 0;
vertical-align: middle;
}
}
/* Styling for the pack edit tab */
#tab-3__content > .content-box {
margin: 1rem 0;
input, ul {
margin-left: 0 }
> div {
margin-top: 1.5rem }
div {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
li {
margin: 0.25rem;
justify-content: space-between;
display: flex;
& button {
margin-left: 10px }
}
li:hover {
color: var(--color-tertiary, rgb(165, 165, 165));
}
}
#tab-3__content {
&:has(#pack-select [value="new"]:checked) .content-box :is(div, ul) {
display: none }
&:has(#pack-select [value="import"]:checked) .pack-box {
display: none }
&:has(#pack-select [value="import"]:checked) .import-box {
display: grid !important }
&:has(#pack-lvl-type [value="internal"]:checked) .pack-lvl-specific {
display: grid !important }
&:has(#pack-lvl-type [value="wk"]:checked) .wk-lvl-warn {
display: grid !important }
}
#pack-items {
background-color: var(--color-menu, white) }
/* Styling for the item edit tab */
#tab-4__content {
hr {
margin: 0;
border-color: var(--color-wk-panel-background, gray)
}
.ctx-sentence-div {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.item-info-edit-container {
padding: 1rem;
border-radius: 3px;
background-color: var(--color-wk-panel-content-background, white);
button {
margin-left: auto }
input, label {
margin-right: 0.5rem }
label {
opacity: 0.5 }
}
.component-div {
display: grid;
grid-template-columns: 1fr 0.2fr;
}
&:has(#item-type [value="Radical"]:checked) .item-radical-specific {
display: grid !important }
&:has(#item-type [value="Kanji"]:checked) .item-kanji-specific {
display: grid !important }
&:has(#item-type [value="Vocabulary"]:checked) .item-vocab-specific {
display: grid !important }
&:has(#item-type [value="KanaVocabulary"]:checked) .item-kanavocab-specific {
display: grid !important }
&:has(#component-type [value="internal"]:checked), &:has(#component-type [value="wk"]:checked) {
#component-type-container {
display: none !important }
#component-id-container {
display: block !important }
}
}
#tab-4__content .content-box, #tab-4__content .content-box > div, #tab-3__content > .content-box, #tab-3__content > .content-box > div {
display: grid;
gap: 0.5rem;
grid-template-columns: 1fr 1fr;
align-items: center;
input, select {
justify-self: end }
}
/* Styling for the settings tab */
#tab-5__content {
label {
margin-right: 1rem;
float: left;
}
}
`;
overviewPopup.innerHTML = /*html*/ `
<header>
<h1>WaniKani Custom SRS</h1>
<button class="close-button" onclick="document.getElementById('overview-popup').close();">${Icons.customIconTxt("cross")}</button>
</header>
<div id="tabs">
<input type="radio" name="custom-srs-tab" id="tab-1" checked>
<label for="tab-1">Overview</label>
<div id="tab-1__content">
<div>
<div class="content-box">
<h2>Lessons</h2>
<p>0</p>
</div>
<div class="content-box">
<h2>Reviews</h2>
<p></p>
</div>
</div>
</div>
<input type="radio" name="custom-srs-tab" id="tab-2">
<label for="tab-2">Packs</label>
<div id="tab-2__content"></div>
<input type="radio" name="custom-srs-tab" id="tab-3">
<label for="tab-3">Edit Pack</label>
<div id="tab-3__content">
<label for="pack-select">Pack:</label>
<select id="pack-select"></select><br>
<form class="content-box pack-box">
<label for="pack-name">Name:</label>
<input id="pack-name" required type="text">
<label for="pack-author">Author: </label>
<input id="pack-author" required type="text">
<label for="pack-version">Version:</label>
<input id="pack-version" required type="number" step="0.1">
<label for="pack-lvl-type">Pack Levelling Type:</label>
<select id="pack-lvl-type" required>
<option value="none">No Levels</option>
<option value="internal">Pack Levels</option>
<option value="wk">WaniKani Levels</option>
</select>
<div class="wk-lvl-warn" style="display: none; grid-column: 1 / span 2; margin: 0; color: red">
<p style="grid-column: 1 / span 2"><i>Warning: Make sure API key is set in Custom SRS settings.</i></p>
</div>
<div class="pack-lvl-specific" style="display: none; grid-column: 1 / span 2; margin: 0">
<label for="pack-lvl">Pack Level (start at 1 recommended):</label>
<input id="pack-lvl" required type="number">
</div>
<div style="grid-column: 1 / span 2">
<p>Items:</p>
<button id="new-item-button" title="Add Item" type="button" style="margin-left: auto">${Icons.customIconTxt("plus")}</button>
</div>
<ul style="grid-column: 1 / span 2" class="content-box" id="pack-items"></ul>
<button style="grid-column: 1 / span 2" type="submit">Save</button>
</form>
<form class="content-box import-box" style="display: none;">
<label for="item-type">Paste Pack JSON here:</label>
<textarea id="pack-import" required></textarea>
<button style="grid-column: 1 / span 2" type="submit">Import</button>
</form>
</div>
<input type="radio" name="custom-srs-tab" id="tab-4">
<label for="tab-4">Edit Item</label>
<div id="tab-4__content">
<div>Select item from Pack edit tab.</div>
<form class="content-box" style="display: none;">
<label for="item-type">Type:</label>
<select id="item-type">
<option value="Radical">Radical</option>
<option value="Kanji">Kanji</option>
<option value="Vocabulary">Vocabulary</option>
<option value="KanaVocabulary">Kana Vocabulary</option>
</select>
<label for="item-characters">Characters:</label>
<input id="item-characters" required type="text">
<label for="item-meanings">Meanings (comma separated):</label>
<input id="item-meanings" required type="text">
<div class="item-vocab-specific item-kanavocab-specific" style="display: none; grid-column: 1 / span 2">
<label for="item-readings">Readings (comma separated):</label>
<input id="item-readings" type="text">
</div>
<div class="item-kanji-specific" style="display: none; grid-column: 1 / span 2">
<label for="knaji-primary-reading">Primary Reading:</label>
<select id="kanji-primary-reading">
<option value="onyomi">On'yomi</option>
<option value="kunyomi">Kun'yomi</option>
<option value="nanori">Nanori</option>
</select>
<p style="grid-column: 1 / span 2"><i>Please enter at least one of the three readings:</i></p>
<label for="kanji-onyomi">On'yomi:</label>
<input id="kanji-onyomi" type="text">
<label for="kanji-kunyomi">Kun'yomi:</label>
<input id="kanji-kunyomi" type="text">
<label for="kanji-nanori">Nanori:</label>
<input id="kanji-nanori" type="text">
</div>
<label for="item-srs-stage">SRS Stage:</label>
<select id="item-srs-stage">
<option value="0">Lesson</option>
<option value="1">Apprentice 1</option>
<option value="2">Apprentice 2</option>
<option value="3">Apprentice 3</option>
<option value="4">Apprentice 4</option>
<option value="5">Guru 1</option>
<option value="6">Guru 2</option>
<option value="7">Master</option>
<option value="8">Enlightened</option>
<option value="9">Burned</option>
</select>
<h3 style="grid-column: 1 / span 2">Optional</h3> <!-- Optional elements -->
<label for="item-level">Item Unlock Level:</label>
<input id="item-level" type="number">
<label for="item-meaning-explanation">Meaning Explanation:</label>
<input id="item-meaning-explanation" type="text">
<div class="item-kanji-specific item-vocab-specific" style="display: none; grid-column: 1 / span 2">
<label for="item-reading-explanation">Reading Explanation:</label>
<input id="item-reading-explanation" type="text">
</div>
<div class="item-info-edit-container item-kanavocab-specific item-vocab-specific" style="display: none; grid-column: 1 / span 2">
<p>Context Sentences</p>
<button id="ctx-add-btn">${Icons.customIconTxt("plus")}</button>
<div id="item-context-sentences-container" style="grid-column: 1 / span 2"></div>
</div>
<div class="item-info-edit-container item-vocab-specific item-radical-specific" style="display: none; grid-column: 1 / span 2">
<p style="grid-column: 1 / span 2">Kanji Components</p>
<span>
<span id="component-type-container">
<label for="component-type" style="float: left">Type:</label>
<select id="component-type">
<option value=""><i>Select type</i></option>
<option value="internal">This Pack</option>
<!--<option value="wk">WaniKani</option>-->
</select>
</span>
<span id="component-id-container" style="display: none">
<label id="component-id-label" for="component-id" style="float: left">Kanji</label>
<input id="component-id" type="text">
</span>
</span>
<button id="component-add-btn">${Icons.customIconTxt("plus")}</button>
<p style="display: none; grid-column: 1 / span 2"><i>Failed to find component.</i></p>
<hr style="grid-column: 1 / span 2">
<div id="components-container" style="grid-column: 1 / span 2"></div>
</div>
<button style="grid-column: 1 / span 2" type="submit">Add</button>
</form>
</div>
<input type="radio" name="custom-srs-tab" id="tab-5">
<label for="tab-5">Settings</label>
<div id="tab-5__content">
<label for="settingsShowDueTime">Show item due times</label>
<input type="checkbox" id="settingsShowDueTime" checked><br>
<label for="settingsExportSRSData">Include SRS data in exports</label>
<input type="checkbox" id="settingsExportSRSData"><br>
<label for="settingsItemQueueMode">Position to insert custom items in reviews</label>
<select id="settingsItemQueueMode">
<option value="start">Start</option>
<option value="weighted-start">Random, weighted towards start</option>
<option value="random">Random</option>
</select><br>
<label for="settingsWKAPIKey">WaniKani API Key</label>
<input type="text" id="settingsWKAPIKey" placeholder="API key">
</div>
</div>
`;
// --------- Popup open button ---------
let overviewPopupButton, buttonLI;
if (window.location.pathname.includes("/dashboard") || window.location.pathname === "/") {
overviewPopupButton = document.createElement("button");
overviewPopupButton.classList = "sitemap__section-header";
overviewPopupButton.style = `
display: flex;
align-items: center;
`;
let buttonSpan = document.createElement("span");
buttonSpan.classList = "font-sans";
buttonSpan.innerText = "WK Custom SRS";
overviewPopupButton.appendChild(buttonSpan);
buttonLI = document.createElement("li");
buttonLI.classList = "sitemap__section";
buttonLI.appendChild(overviewPopupButton);
overviewPopupButton.title = "Custom SRS";
overviewPopupButton.onclick = () => {
changeTab(1);
overviewPopup.showModal();
};
// --------- Add custom elements to page ---------
document.addEventListener("DOMContentLoaded", () => {
document.head.appendChild(overviewPopupStyle);
document.body.appendChild(overviewPopup);
// Add event listeners for buttons etc.
for(let i = 1; i <= 5; i++) {
document.querySelector(`#tab-${i}`).onchange = () => {
changeTab(i) };
}
document.querySelector("#pack-select").onchange = () => {
loadPackEditDetails(document.querySelector("#pack-select").value) };
document.querySelector("#new-item-button").onclick = () => {
changeTab(4, null) };
// Add popup button to page
if (window.location.pathname.includes("/dashboard") || window.location.pathname === "/") {
document.querySelector("#sitemap").prepend(buttonLI);
}
});
}
// ---------- Change tab ----------
function changeTab(tab, data) {
document.querySelector(`#tab-${tab}`).checked = true;
switch(tab) {
case 1:
updateOverviewTab();
break;
case 2:
updatePacksTab();
break;
case 3:
updateEditPackTab(data);
break;
case 4:
updateEditItemTab(data);
break;
case 5:
updateSettingsTab();
break;
}
}
// ---------- Update popup content ----------
function updateOverviewTab() {
//document.querySelector("#tab-1__content .content-box:first-child p").innerText = activePackProfile.getActiveLessons().length;
document.querySelector("#tab-1__content .content-box:last-child p").innerText = activePackProfile.getNumActiveReviews();
}
function updatePacksTab() {
let packsTab = document.querySelector("#tab-2__content");
packsTab.innerHTML = "";
for(let i = 0; i < activePackProfile.customPacks.length; i++) {
let pack = activePackProfile.customPacks[i];
let packElement = document.createElement("div");
packElement.classList = "pack content-box";
packElement.innerHTML = /*html*/ `
<h3>${pack.name}: <span>${pack.items.length} items</span><br><span>${pack.author}</span></h3>
<div>
<span>Active: </span>
<input type="checkbox" id="pack-${i}-active" ${pack.active ? "checked" : ""}>
<button class="edit-pack" title="Edit Pack">${Icons.customIconTxt("edit")}</button>
<button class="export-pack" title="Export Pack">${Icons.customIconTxt("download")}</button>
<button class="delete-pack" title="Delete Pack">${Icons.customIconTxt("cross")}</button>
</div>
`;
packElement.querySelector(".edit-pack").onclick = () => { // Pack edit button
changeTab(3, i);
};
packElement.querySelector(".export-pack").onclick = () => { // Pack export button to make JSON and then copy it to the clipboard
let data = StorageManager.packToJSON(activePackProfile.customPacks[i]);
navigator.clipboard.writeText(data).then(() => {
alert("Pack JSON copied to clipboard");
});
};
packElement.querySelector(".delete-pack").onclick = () => { // Pack delete button
activePackProfile.removePack(i);
StorageManager.savePackProfile(activePackProfile, "main");
changeTab(2);
};
packElement.querySelector(`#pack-${i}-active`).onchange = () => { // Pack active checkbox
activePackProfile.customPacks[i].active = !activePackProfile.customPacks[i].active;
StorageManager.savePackProfile(activePackProfile, "main");
};
packsTab.appendChild(packElement);
}
// New pack button
let newPackButton = document.createElement("button");
newPackButton.classList = "outline-button";
newPackButton.style = "width: 48%";
newPackButton.innerHTML = "New Pack";
newPackButton.onclick = () => {
changeTab(3, "new");
};
let importPackButton = document.createElement("button");
importPackButton.classList = "outline-button";
importPackButton.style = "width: 48%; float: right;";
importPackButton.innerHTML = "Import Pack";
importPackButton.onclick = () => {
changeTab(3, "import");
};
packsTab.append(newPackButton, importPackButton);
}
function updateEditPackTab(editPack) {
let packSelect = document.querySelector("#pack-select");
packSelect.innerHTML = "<option value='new'>New Pack</option><option value='import'>Import Pack</option>";
for(let i = 0; i < activePackProfile.customPacks.length; i++) {
let pack = activePackProfile.customPacks[i];
packSelect.innerHTML += `<option value="${i}">${pack.name} - ${pack.author}</option>`;
}
if(editPack !== undefined) packSelect.value = editPack;
else packSelect.value = "new";
packSelect.onchange();
}
function updateEditItemTab(editItem) {
if(editItem !== undefined) {
// Show add item edit tab and make sure inputs are empty
document.querySelector("#tab-4__content > form").style.display = "grid";
document.querySelector("#tab-4__content > div").style.display = "none";
document.getElementById("ctx-add-btn").onclick = (e) => {
e.preventDefault();
document.getElementById("item-context-sentences-container").innerHTML += buildContextSentenceEditHTML("", "");
};
document.getElementById("component-add-btn").onclick = (e) => { // Handle adding kanji components
e.preventDefault();
let type = document.getElementById("component-type").value;
let id = document.getElementById("component-id").value;
if(type === "" || id === "") return;
// Check if component exists. When type is internal id is the item character to search for
switch(type) {
case "internal":
let type = document.getElementById("component-id-label").innerText;
let itemID = activePackProfile.customPacks[document.querySelector("#pack-select").value].getItemID(type, document.getElementById("component-id").value);
if(itemID) {
document.getElementById("component-add-btn").nextElementSibling.style.display = "none";
document.getElementById("components-container").innerHTML += buildKanjiComponentEditHTML(type, document.querySelector("#pack-select").value, itemID);
document.getElementById("component-type").value = "";
document.getElementById("component-id").value = "";
} else {
document.getElementById("component-add-btn").nextElementSibling.style.display = "block";
}
break;
case "wk":
// TODO: Add WaniKani component check
break;
}
};
if(editItem !== null) {
let editItemInfo = activePackProfile.customPacks[document.querySelector("#pack-select").value].getItem(editItem).info;
document.querySelector("#item-srs-stage").value = editItemInfo.srs_lvl;
document.querySelector("#item-type").value = editItemInfo.type;
document.querySelector("#item-characters").value = editItemInfo.characters;
document.querySelector("#item-meanings").value = editItemInfo.meanings.join(", ");
if(editItemInfo.lvl) document.querySelector("#item-level").value = editItemInfo.lvl;
if(editItemInfo.meaning_expl) document.querySelector("#item-meaning-explanation").value = editItemInfo.meaning_expl;
if(editItemInfo.readings) document.querySelector("#item-readings").value = editItemInfo.readings.join(", ");
if(editItemInfo.primary_reading_type) document.querySelector("#kanji-primary-reading").value = editItemInfo.primary_reading_type;
if(editItemInfo.onyomi) document.querySelector("#kanji-onyomi").value = editItemInfo.onyomi.join(", ");
if(editItemInfo.kunyomi) document.querySelector("#kanji-kunyomi").value = editItemInfo.kunyomi.join(", ");
if(editItemInfo.nanori) document.querySelector("#kanji-nanori").value = editItemInfo.nanori.join(", ");
if(editItemInfo.reading_expl) document.querySelector("#item-reading-explanation").value = editItemInfo.reading_expl;
if(editItemInfo.ctx_jp) {
document.getElementById("item-context-sentences-container").innerHTML = editItemInfo.ctx_jp.map((s, i) => {
return buildContextSentenceEditHTML(s, editItemInfo.ctx_en[i]);
}).join("");
}
if(editItemInfo.kanji) {
document.getElementById("components-container").innerHTML = editItemInfo.kanji.map((k) => {
return buildKanjiComponentEditHTML(k[0], k[1], k[2]);
}).join("");
}
document.querySelector("#tab-4__content button[type='submit']").innerText = "Save";
} else {
["item-reading-explanation", "item-meaning-explanation", "item-characters", "item-meanings", "item-readings", "kanji-onyomi", "kanji-kunyomi", "item-level", "kanji-nanori"].forEach((s) => {
document.getElementById(s).value = "";
});
document.querySelector("#tab-4__content button[type='submit']").innerText = "Add";
document.querySelector("#item-srs-stage").value = "0";
document.querySelector("#item-context-sentences-container").innerHTML = "";
document.querySelector("#components-container").innerHTML = "";
}
// Add event listener to form
document.querySelector("#tab-4__content form").onsubmit = (e) => {
e.preventDefault();
let itemType = document.querySelector("#item-type").value;
let infoStruct = {
type: itemType,
characters: document.querySelector("#item-characters").value,
meanings: document.querySelector("#item-meanings").value.split(",").map(s => s.trim()),
srs_lvl: document.querySelector("#item-srs-stage").value
};
if(document.querySelector("#item-meaning-explanation").value != "") infoStruct.meaning_expl = document.querySelector("#item-meaning-explanation").value;
if(document.querySelector("#item-level").value != "") infoStruct.lvl = parseInt(document.querySelector("#item-level").value);
let pack = activePackProfile.customPacks[document.querySelector("#pack-select").value];
let ctxDivs = document.querySelector("#item-context-sentences-container").children;
// Add or edit item
switch(itemType) {
case "Radical":
infoStruct.category = infoStruct.type;
if(document.getElementById("components-container").children.length > 0) {
infoStruct.kanji = [];
let container = document.getElementById("components-container");
for(let i = 0; i < container.children.length; i++) {
let [type, pack, id] = container.children[i].querySelector(".component-info").innerText.split(",");
infoStruct.kanji.push([type, parseInt(pack), parseInt(id)]);
}
}
break;
case "Kanji":
infoStruct.category = infoStruct.type;
infoStruct.primary_reading_type = document.querySelector("#kanji-primary-reading").value;
if(document.querySelector("#kanji-onyomi").value != "") infoStruct.onyomi = document.querySelector("#kanji-onyomi").value.split(",").map(s => s.trim());
if(document.querySelector("#kanji-kunyomi").value != "") infoStruct.kunyomi = document.querySelector("#kanji-kunyomi").value.split(",").map(s => s.trim());
if(document.querySelector("#kanji-nanori").value != "") infoStruct.nanori = document.querySelector("#kanji-nanori").value.split(",").map(s => s.trim());
if(document.querySelector("#item-reading-explanation").value != "") infoStruct.reading_expl = document.querySelector("#item-reading-explanation").value;
break;
case "Vocabulary":
infoStruct.category = infoStruct.type;
infoStruct.readings = document.querySelector("#item-readings").value.split(",").map(s => s.trim());
if(document.querySelector("#item-reading-explanation").value != "") infoStruct.reading_expl = document.querySelector("#item-reading-explanation").value;
if(document.querySelector("#item-context-sentences-container").children.length > 0) {
infoStruct.ctx_jp = [];
infoStruct.ctx_en = [];
for(let i = 0; i < ctxDivs.length; i++) {
let ctxDiv = ctxDivs[i];
infoStruct.ctx_jp.push(ctxDiv.children[0].value);
infoStruct.ctx_en.push(ctxDiv.children[1].value);
}
}
if(document.getElementById("components-container").children.length > 0) {
infoStruct.kanji = [];
let container = document.getElementById("components-container");
for(let i = 0; i < container.children.length; i++) {
let [type, pack, id] = container.children[i].querySelector(".component-info").innerText.split(",");
infoStruct.kanji.push([type, parseInt(pack), parseInt(id)]);
}
}
break;
case "KanaVocabulary":
infoStruct.category = "Vocabulary";
infoStruct.readings = document.querySelector("#item-readings").value.split(",").map(s => s.trim());
if(document.querySelector("#item-context-sentences-container").children.length > 0) {
infoStruct.ctx_jp = [];
infoStruct.ctx_en = [];
for(let i = 0; i < ctxDivs.length; i++) {
let ctxDiv = ctxDivs[i];
infoStruct.ctx_jp.push(ctxDiv.children[0].value);
infoStruct.ctx_en.push(ctxDiv.children[1].value);
}
}
break;
default:
console.error("Invalid item type");
return;
}
if(editItem !== null) pack.editItem(editItem, infoStruct);
else pack.addItem(infoStruct);
document.querySelector("#tab-4__content > form").style.display = "none";
document.querySelector("#tab-4__content > div").style.display = "block";
loadPackEditDetails(document.querySelector("#pack-select").value);
StorageManager.savePackProfile(activePackProfile, "main");
changeTab(3, document.querySelector("#pack-select").value);
};
} else {
// Hide add item edit tab
document.querySelector("#tab-4__content > form").style.display = "none";
document.querySelector("#tab-4__content > div").style.display = "block";
}
}
function updateSettingsTab() {
document.querySelector("#settingsShowDueTime").checked = CustomSRSSettings.userSettings.showItemDueTime;
document.querySelector("#settingsShowDueTime").onchange = () => {
CustomSRSSettings.userSettings.showItemDueTime = document.querySelector("#settingsShowDueTime").checked;
StorageManager.saveSettings();
};
document.querySelector("#settingsItemQueueMode").value = CustomSRSSettings.userSettings.itemQueueMode ? CustomSRSSettings.userSettings.itemQueueMode : "start";
document.querySelector("#settingsItemQueueMode").onchange = () => {
CustomSRSSettings.userSettings.itemQueueMode = document.querySelector("#settingsItemQueueMode").value;
StorageManager.saveSettings();
};
document.querySelector("#settingsExportSRSData").checked = CustomSRSSettings.userSettings.exportSRSData;
document.querySelector("#settingsExportSRSData").onchange = () => {
CustomSRSSettings.userSettings.exportSRSData = document.querySelector("#settingsExportSRSData").checked;
StorageManager.saveSettings();
};
document.querySelector("#settingsWKAPIKey").value = CustomSRSSettings.userSettings.apiKey;
document.querySelector("#settingsWKAPIKey").onchange = () => {
CustomSRSSettings.userSettings.apiKey = document.querySelector("#settingsWKAPIKey").value;
StorageManager.saveSettings();
};
}
// ---------- Tabs details ----------
function loadPackEditDetails(i) {
let packNameInput = document.querySelector("#pack-name");
let packAuthorInput = document.querySelector("#pack-author");
let packVersionInput = document.querySelector("#pack-version");
let packLvlTypeInput = document.querySelector("#pack-lvl-type");
let packLvlInput = document.querySelector("#pack-lvl");
let packItems = document.querySelector("#pack-items");
let importBox = document.querySelector("#pack-import");
if(i === "new") { // If creating a new pack
packNameInput.value = "";
packAuthorInput.value = "";
packVersionInput.value = 0.1;
packLvlTypeInput.value = "none";
packLvlInput.value = 1;
} else if(i === "import") { // If importing a pack
importBox.value = "";
} else { // If editing an existing pack
let pack = activePackProfile.customPacks[i];
packNameInput.value = pack.name;
packAuthorInput.value = pack.author;
packVersionInput.value = pack.version;
packLvlTypeInput.value = pack.lvlType;
packLvlInput.value = pack.lvl;
packItems.innerHTML = "";
for(let j = 0; j < pack.items.length; j++) {
let item = pack.items[j];
let itemElement = document.createElement("li");
itemElement.classList = "pack-item";
itemElement.innerHTML = `
${item.info.characters} - ${item.info.meanings[0]} - ${item.info.type} ${CustomSRSSettings.userSettings.showItemDueTime ? "- Due: " + pack.getItemTimeUntilReview(j) : ""}
<div>
<button class="edit-item" title="Edit Item" type="button">${Icons.customIconTxt("edit")}</button>
<button class="delete-item" title="Delete Item" type="button">${Icons.customIconTxt("cross")}</button>
</div>
`;
itemElement.querySelector(".edit-item").onclick = () => { // Item edit button
savePack();
changeTab(4, item.id);
};
itemElement.querySelector(".delete-item").onclick = () => { // Item delete button
pack.removeItem(j);
loadPackEditDetails(i);
};
packItems.appendChild(itemElement);
}
}
document.querySelector("#tab-3__content form.pack-box").onsubmit = (e) => { // Pack save button
e.preventDefault();
savePack();
changeTab(2);
};
document.querySelector("#tab-3__content form.import-box").onsubmit = (e) => { // Pack import button
e.preventDefault();
let pack = JSON.parse(importBox.value);
let packExistingStatus = activePackProfile.doesPackExist(pack.name, pack.author, pack.version); // Check if pack already exists
if(packExistingStatus == "exists") {
alert("Import failed: A pack with the same name, author, and version already exists.");
} else if(packExistingStatus == "no") {
activePackProfile.addPack(StorageManager.packFromJSON(pack));
StorageManager.savePackProfile(activePackProfile, "main");
changeTab(2);
} else {
if(confirm("A pack with the same name and author but different version already exists. Do you want to update it?")) {
activePackProfile.updatePack(packExistingStatus, pack);
StorageManager.savePackProfile(activePackProfile, "main");
changeTab(2);
}
}
};
function savePack() {
if(i === "new") {
let pack = new CustomItemPack(packNameInput.value, packAuthorInput.value, packVersionInput.value, packLvlTypeInput.value, parseInt(packLvlInput.value));
activePackProfile.addPack(pack);
changeTab(3, activePackProfile.customPacks.length - 1);
} else {
activePackProfile.customPacks[i].name = packNameInput.value;
activePackProfile.customPacks[i].author = packAuthorInput.value;
activePackProfile.customPacks[i].version = packVersionInput.value;
activePackProfile.customPacks[i].lvlType = packLvlTypeInput.value;
activePackProfile.customPacks[i].lvl = packLvlInput.value;
}
StorageManager.savePackProfile(activePackProfile, "main");
}
}
// ---------- Item info procedural edit structures ----------
function buildKanjiComponentEditHTML(type, pack, id) {
return /*html*/ `
<div class="component-div">
<p>${type == "wk" ? "WaniKani" : "This Pack"} Kanji. ID: ${id} Character: ${activePackProfile.getPack(pack).getItem(id).info.characters}</p>
<button class="delete-component" title="Delete Component" onclick="this.parentElement.remove()">${Icons.customIconTxt("cross")}</button>
<span class="component-info" style="display: none">${type},${pack},${id}</span>
</div>
`;
}
function buildContextSentenceEditHTML(jp, en) {
return /*html*/ `
<div class="ctx-sentence-div">
<input type="text" value="${jp}" placeholder="Japanese" required>
<input type="text" value="${en}" placeholder="English" required>
<button class="delete-sentence" title="Delete Sentence" onclick="this.parentElement.remove()">${Icons.customIconTxt("cross")}</button>
</div>
`;
}
// ---------- Item details ----------
function buildKanjiComponentHTML(type, pack, id) {
let item;
if(pack >= 0) item = activePackProfile.getPack(pack).getItem(id);
else item = null; // TODO: Add WaniKani component check
return /*html*/ `
<li class="subject-character-grid__item">
<a class="subject-character subject-character--${type.toLowerCase()} subject-character--grid ${item.info.srs_lvl > 8 ? "subject-character--burned" : ""}" data-turbo-frame="_blank">
<div class="subject-character__content">
<span class="subject-character__characters" lang="ja">${item.info.characters}</span>
<div class="subject-character__info">
<span class="subject-character__reading">${item.primary_reading_type == "onyomi" ? item.info.onyomi[0] : item.primary_reading_type == "kunyomi" ? item.info.kunyomi[0] : item.info.nanori[0]}</span>
<span class="subject-character__meaning">${item.info.meanings[0]}</span>
</div>
</div>
</a>
</li>
`;
}
function buildContextSentencesHTML(ctxArrayJP, ctxArrayEN) {
let out = "";
for(let i = 0; i < ctxArrayJP.length; i++) {
out += `
<div class="subject-section__text subject-section__text--grouped">
<p lang="ja">${ctxArrayJP[i]}</p>
<p>${ctxArrayEN[i]}</p>
</div>
`;
}
return out;
}
function makeDetailsHTML(item) {
switch(item.info.type) {
case "Radical":
return /*html*/ `
<turbo-frame class="subject-info" id="subject-info">
<div class="container">
<section class="subject-section subject-section--meaning subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["meaning"]}">
<a class='wk-nav__anchor' id='information'></a>
<h2 class='subject-section__title'>
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-meaning">
<span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
<span class='subject-section__title-text'>Name</span>
</a>
</h2>
<section id="section-meaning" class="subject-section__content" data-toggle-target="content" hidden="hidden">
<section class="subject-section__subsection">
<div class='subject-section__meanings'>
<h2 class='subject-section__meanings-title'>Primary</h2>
<p class='subject-section__meanings-items'>${item.info.meanings[0]}</p>
</div>
${item.info.meanings.length > 1 ? `
<div class="subject-section__meanings">
<h2 class="subject-section__meanings-title">Alternatives</h2>
<p class="subject-section__meanings-items">${item.info.meanings.slice(1).join(', ')}</p>
</div>` : ''}
<!--<div class='subject-section__meanings'>
<h2 class='subject-section__meanings-title'>User Synonyms</h2>
<p class='subject-section__meanings-items'><i>User synonyms are currently disabled for custom items.</i></p>
</div>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Mnemonic</h3>
<p class="subject-section__text">${item.info.meaning_expl ? item.info.meaning_expl : "This item does not have a meaning explanation. Good luck!"}</p>
<!--<aside class="subject-hint">
<h3 class="subject-hint__title">
<i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
<span class="subject-hint__title-text">Hints</span>
</h3>
<p class="subject-hint__text"></p>
</aside>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Note</h3>
<p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
</section>
</section>
</section>
<section class="subject-section subject-section--amalgamations subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":[]}">
<a class='wk-nav__anchor' id='amalgamations'></a>
<h2 class='subject-section__title'>
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-amalgamations">
<span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
<span class='subject-section__title-text'>Found In Kanji</span>
</a>
</h2>
<section id="section-amalgamations" class="subject-section__content" data-toggle-target="content" hidden="hidden">
<div class="subject-character-grid">
<ol class="subject-character-grid__items">
${item.info.kanji ? item.info.kanji.map(k => buildKanjiComponentHTML(k[0], k[1], k[2])).join('') : "No found in kanji set."}
</ol>
</div>
</section>
</section>
</div>
</turbo-frame>
`;
case "Kanji":
return /*html*/ `
<turbo-frame class="subject-info" id="subject-info">
<div class="container">
<!-- Radical combination -->
<section class="subject-section subject-section--components subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["meaning"]}">
<h2 class="subject-section__title">
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-components" data-controller-connected="true">
<span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
<span class="subject-section__title-text">Radical Combination</span>
</a>
</h2>
<section id="section-components" class="subject-section__content" data-toggle-target="content">
<div class="subject-list subject-list--with-separator">
<ul class="subject-list__items">
<!--<li class="subject-list__item">
<a class="subject-character subject-character--radical subject-character--small-with-meaning subject-character--burned subject-character--expandable" title="Head" href="https://www.wanikani.com/radicals/head" data-turbo-frame="_blank">
<div class="subject-character__content">
<span class="subject-character__characters" lang="ja">冂</span>
<div class="subject-character__info">
<span class="subject-character__meaning">Head</span>
</div>
</div>
</a>
</li>-->
</ul>
</div>
</section>
</section>
<!-- Meaning -->
<section class="subject-section subject-section--meaning subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["meaning"]}">
<a class='wk-nav__anchor' id='meaning'></a>
<h2 class='subject-section__title'>
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-meaning">
<span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
<span class='subject-section__title-text'>Meaning</span>
</a>
</h2>
<section id="section-meaning" class="subject-section__content" data-toggle-target="content" hidden="hidden">
<section class="subject-section__subsection">
<div class='subject-section__meanings'>
<h2 class='subject-section__meanings-title'>Primary</h2>
<p class='subject-section__meanings-items'>${item.info.meanings[0]}</p>
</div>
${item.info.meanings.length > 1 ? `
<div class="subject-section__meanings">
<h2 class="subject-section__meanings-title">Alternative</h2>
<p class="subject-section__meanings-items">${item.info.meanings.slice(1).join(', ')}</p>
</div>` : ''}
<!--<div class='subject-section__meanings'>
<h2 class='subject-section__meanings-title'>User Synonyms</h2>
<p class='subject-section__meanings-items'><i>User synonyms are currently disabled for custom items.</i></p>
</div>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Mnemonic</h3>
<p class="subject-section__text">${item.info.meaning_expl ? item.info.meaning_expl : "This item does not have a reading explanation. Good luck!"}</p>
<!--<aside class="subject-hint">
<h3 class="subject-hint__title">
<i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
<span class="subject-hint__title-text">Hints</span>
</h3>
<p class="subject-hint__text"></p>
</aside>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Note</h3>
<p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
</section>
</section>
</section>
<!-- Reading -->
<section class="subject-section subject-section--reading subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["reading"]}">
<a class='wk-nav__anchor' id='reading'></a>
<h2 class='subject-section__title'>
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-reading">
<span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
<span class='subject-section__title-text'>Reading</span>
</a>
</h2>
<section id="section-reading" class="subject-section__content" data-toggle-target="content" hidden="hidden">
<section class="subject-section__subsection">
<div class="subject-readings">
<div class="subject-readings__reading ${item.info.primary_reading_type == "onyomi" ? "subject-readings__reading--primary" : ""}">
<h3 class="subject-readings__reading-title">On’yomi</h3>
<p class="subject-readings__reading-items" lang="ja">
${item.info.onyomi && item.info.onyomi.length > 0 ? item.info.onyomi.join(', ') : "None"}
</p>
</div>
<div class="subject-readings__reading ${item.info.primary_reading_type == "kunyomi" ? "subject-readings__reading--primary" : ""}">
<h3 class="subject-readings__reading-title">Kun’yomi</h3>
<p class="subject-readings__reading-items" lang="ja">
${item.info.kunyomi && item.info.kunyomi.length > 0 ? item.info.kunyomi.join(', ') : "None"}
</p>
</div>
<div class="subject-readings__reading ${item.info.primary_reading_type == "nanori" ? "subject-readings__reading--primary" : ""}">
<h3 class="subject-readings__reading-title">Nanori</h3>
<p class="subject-readings__reading-items" lang="ja">
${item.info.nanori && item.info.nanori.length > 0 ? item.info.nanori.join(', ') : "None"}
</p>
</div>
</div>
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Mnemonic</h3>
<p class="subject-section__text">${item.info.reading_expl ? item.info.reading_expl : "This item does not have a reading explanation. Good luck!"}</p>
<!--<aside class="subject-hint">
<h3 class="subject-hint__title">
<i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
<span class="subject-hint__title-text">Hints</span>
</h3>
<p class="subject-hint__text"></p>
</aside>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Note</h3>
<p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
</section>
</section>
</section>
<!-- Found in vocabulary -->
<section class="subject-section subject-section--amalgamations subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":[]}">
<a class="wk-nav__anchor" id="amalgamations"></a>
<h2 class="subject-section__title">
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-amalgamations" data-controller-connected="true">
<span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
<span class="subject-section__title-text">Found In Vocabulary</span>
</a>
</h2>
<section id="section-amalgamations" class="subject-section__content" data-toggle-target="content">
<div class="subject-character-grid subject-character-grid--single-column">
<ol class="subject-character-grid__items">
<!--<li class="subject-character-grid__item">
<a class="subject-character subject-character--vocabulary subject-character--grid subject-character--burned" title="うち" href="https://www.wanikani.com/vocabulary/%E5%86%85" data-turbo-frame="_blank">
<div class="subject-character__content">
<span class="subject-character__characters" lang="ja">内</span>
<div class="subject-character__info">
<span class="subject-character__reading">うち</span>
<span class="subject-character__meaning">Inside</span>
</div>
</div>
</a>
</li>-->
</ol>
</div>
</section>
</section>
</div>
</turbo-frame>
`;
case "Vocabulary":
return /*html*/ `
<turbo-frame class="subject-info" id="subject-info">
<div class="container">
<!-- Meaning -->
<section class="subject-section subject-section--meaning subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["meaning"]}">
<a class='wk-nav__anchor' id='meaning'></a>
<h2 class='subject-section__title'>
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-meaning">
<span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
<span class='subject-section__title-text'>Meaning</span>
</a>
</h2>
<section id="section-meaning" class="subject-section__content" data-toggle-target="content" hidden="hidden">
<section class="subject-section__subsection">
<div class='subject-section__meanings'>
<h2 class='subject-section__meanings-title'>Primary</h2>
<p class='subject-section__meanings-items'>${item.info.meanings[0]}</p>
</div>
${item.info.meanings.length > 1 ? `
<div class="subject-section__meanings">
<h2 class="subject-section__meanings-title">Alternatives</h2>
<p class="subject-section__meanings-items">${item.info.meanings.slice(1).join(', ')}</p>
</div>` : ''}
<!--<div class='subject-section__meanings'>
<h2 class='subject-section__meanings-title'>User Synonyms</h2>
<p class='subject-section__meanings-items'><i>User synonyms are currently disabled for custom items.</i></p>
</div>-->
<!--<div class="subject-section__meanings">
<h2 class="subject-section__meanings-title">Word Type</h2>
<p class="subject-section__meanings-items">noun, の adjective</p>
</div>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Explanation</h3>
<p class="subject-section__text">${item.info.meaning_expl ? item.info.meaning_expl : "This item does not have a meaning explanation. Good luck!"}</p>
<!--<aside class="subject-hint">
<h3 class="subject-hint__title">
<i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
<span class="subject-hint__title-text">Hints</span>
</h3>
<p class="subject-hint__text"></p>
</aside>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Note</h3>
<p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
</section>
</section>
</section>
<!-- Reading -->
<section class="subject-section subject-section--reading subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["reading"]}">
<a class='wk-nav__anchor' id='reading'></a>
<h2 class='subject-section__title'>
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-reading">
<span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
<span class='subject-section__title-text'>Reading</span>
</a>
</h2>
<section id="section-reading" class="subject-section__content" data-toggle-target="content" hidden="hidden">
<section class="subject-section__subsection">
<div class="subject-readings-with-audio">
<div class="subject-readings-with-audio__item">
<div class="reading-with-audio">
<div class="reading-with-audio__reading" lang='ja'>${item.info.readings[0]}</div>
<ul class="reading-with-audio__audio-items">
</ul>
</div>
</div>
</div>
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Explanation</h3>
<p class="subject-section__text">${item.info.reading_expl ? item.info.reading_expl : "This item does not have a reading explanation. Good luck!"}</p>
<!--<aside class="subject-hint">
<h3 class="subject-hint__title">
<i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
<span class="subject-hint__title-text">Hints</span>
</h3>
<p class="subject-hint__text"></p>
</aside>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Note</h3>
<p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
</section>
</section>
</section>
<!-- Context -->
<section class="subject-section subject-section--context subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["meaning"]}">
<a class="wk-nav__anchor" id="context"></a>
<h2 class="subject-section__title">
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-context" data-controller-connected="true">
<span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
<span class="subject-section__title-text">Context</span>
</a>
</h2>
<section id="section-context" class="subject-section__content" data-toggle-target="content">
<!--<section class="subject-section__subsection">
<div class="subject-collocations" data-controller="tabbed-content" data-tabbed-content-next-tab-hotkey-value="s" data-tabbed-content-previous-tab-hotkey-value="w" data-hotkey-registered="true">
<div class="subject-collocations__patterns">
<h3 class="subject-collocations__title subject-collocations__title--patterns">Pattern of Use</h3>
<div class="subject-collocations__pattern-names">
<a class="subject-collocations__pattern-name" data-tabbed-content-target="tab" data-action="tabbed-content#changeTab" aria-controls="collocations-710736400-0" aria-selected="true" role="tab" lang="ja" href="#collocations-710736400-0">農業を〜</a>
</div>
</div>
<div class="subject-collocations__collocations">
<h3 class="subject-collocations__title">Common Word Combinations</h3>
<ul class="subject-collocations__pattern-collocations">
<li class="subject-collocations__pattern-collocation" id="collocations-710736400-0" data-tabbed-content-target="content" role="tabpanel">
<div class="context-sentences">
<p class="wk-text" lang="ja">農業を行う</p>
<p class="wk-text">to carry out farming</p>
</div>
</li>
</ul>
</div>
</div>
</section>-->
<section class="subject-section__subsection">
<h3 class="subject-section__subtitle">Context Sentences</h3>
${item.info.ctx_jp ? buildContextSentencesHTML(item.info.ctx_jp, item.info.ctx_en) : "No context sentences set."}
</section>
</section>
</section>
<!-- Kanji Composition -->
<section class="subject-section subject-section--components subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":[]}">
<a class="wk-nav__anchor" id="components"></a>
<h2 class="subject-section__title">
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-components" data-controller-connected="true">
<span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
<span class="subject-section__title-text">Kanji Composition</span>
</a>
</h2>
<section id="section-components" class="subject-section__content" data-toggle-target="content">
<div class="subject-character-grid">
<ol class="subject-character-grid__items">
${item.info.kanji ? item.info.kanji.map(k => buildKanjiComponentHTML(k[0], k[1], k[2])).join('') : "No kanji components set."}
</ol>
</div>
</section>
</section>
</div>
</turbo-frame>
`;
case "KanaVocabulary":
return /*html*/ `
<turbo-frame class="subject-info" id="subject-info">
<div class="container">
<!-- Meaning -->
<section class="subject-section subject-section--meaning subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["meaning"]}">
<a class='wk-nav__anchor' id='meaning'></a>
<h2 class='subject-section__title'>
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-meaning">
<span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
<span class='subject-section__title-text'>Meaning</span>
</a>
</h2>
<section id="section-meaning" class="subject-section__content" data-toggle-target="content" hidden="hidden">
<section class="subject-section__subsection">
<div class='subject-section__meanings'>
<h2 class='subject-section__meanings-title'>Primary</h2>
<p class='subject-section__meanings-items'>${item.info.meanings[0]}</p>
</div>
${item.info.meanings.length > 1 ? `
<div class="subject-section__meanings">
<h2 class="subject-section__meanings-title">Alternatives</h2>
<p class="subject-section__meanings-items">${item.info.meanings.slice(1).join(', ')}</p>
</div>` : ''}
<!--<div class='subject-section__meanings'>
<h2 class='subject-section__meanings-title'>User Synonyms</h2>
<p class='subject-section__meanings-items'><i>User synonyms are currently disabled for custom items.</i></p>
</div>-->
<!--<div class="subject-section__meanings">
<h2 class="subject-section__meanings-title">Word Type</h2>
<p class="subject-section__meanings-items">noun, suffix</p>
</div>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Explanation</h3>
<p class="subject-section__text">${item.info.meaning_expl ? item.info.meaning_expl : "This item does not have a meaning explanation. Good luck!"}</p>
<!--<aside class="subject-hint">
<h3 class="subject-hint__title">
<i class="subject-hint__title-icon" aria-hidden="true">${Icons.customIconTxt("circle-info")}</i>
<span class="subject-hint__title-text">Hints</span>
</h3>
<p class="subject-hint__text"></p>
</aside>-->
</section>
<section class="subject-section__subsection">
<h3 class='subject-section__subtitle'>Note</h3>
<p class="subject-section__text"><i>Notes are currently disabled for custom items.</i></p>
</section>
</section>
</section>
<!-- Pronunciation -->
<section class="subject-section subject-section--reading subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["reading"]}">
<a class='wk-nav__anchor' id='reading'></a>
<h2 class='subject-section__title'>
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-reading">
<span class="subject-section__toggle-icon">${Icons.customIconTxt("chevron-right")}</span>
<span class='subject-section__title-text'>Pronunciation</span>
</a>
</h2>
<section id="section-reading" class="subject-section__content" data-toggle-target="content" hidden="hidden">
<section class="subject-section__subsection">
<div class="subject-readings-with-audio">
<div class="subject-readings-with-audio__item">
<div class="reading-with-audio">
<div class="reading-with-audio__reading" lang='ja'>${item.info.readings[0]}</div>
<ul class="reading-with-audio__audio-items">
</ul>
</div>
</div>
</div>
</section>
</section>
</section>
<!-- Context -->
<section class="subject-section subject-section--context subject-section--collapsible" data-controller="toggle" data-toggle-context-value="{"auto_expand_question_types":["meaning"]}">
<a class="wk-nav__anchor" id="context"></a>
<h2 class="subject-section__title">
<a class="subject-section__toggle" data-toggle-target="toggle" data-action="toggle#toggle" aria-expanded="false" aria-controls="section-context" data-controller-connected="true">
<span class="subject-section__toggle-icon" aria-hidden="true">${Icons.customIconTxt("chevron-right")}</span>
<span class="subject-section__title-text">Context</span>
</a>
</h2>
<section id="section-context" class="subject-section__content" data-toggle-target="content">
<!--<section class="subject-section__subsection">
<div class="subject-collocations" data-controller="tabbed-content" data-tabbed-content-next-tab-hotkey-value="s" data-tabbed-content-previous-tab-hotkey-value="w" data-hotkey-registered="true">
<div class="subject-collocations__patterns">
<h3 class="subject-collocations__title subject-collocations__title--patterns">Pattern of Use</h3>
<div class="subject-collocations__pattern-names">
<a class="subject-collocations__pattern-name" data-tabbed-content-target="tab" data-action="tabbed-content#changeTab" aria-controls="collocations-710736400-0" aria-selected="true" role="tab" lang="ja" href="#collocations-710736400-0">農業を〜</a>
</div>
</div>
<div class="subject-collocations__collocations">
<h3 class="subject-collocations__title">Common Word Combinations</h3>
<ul class="subject-collocations__pattern-collocations">
<li class="subject-collocations__pattern-collocation" id="collocations-710736400-0" data-tabbed-content-target="content" role="tabpanel">
<div class="context-sentences">
<p class="wk-text" lang="ja">農業を行う</p>
<p class="wk-text">to carry out farming</p>
</div>
</li>
</ul>
</div>
</div>
</section>-->
<section class="subject-section__subsection">
<h3 class="subject-section__subtitle">Context Sentences</h3>
${item.info.ctx_jp ? buildContextSentencesHTML(item.info.ctx_jp, item.info.ctx_en) : "No context sentences set."}
</section>
</section>
</section>
</div>
</turbo-frame>
`;
}
}
const srsGaps = [0, 4*60*60*1000, 8*60*60*1000, 23*60*60*1000, 47*60*60*1000, 167*60*60*1000, 335*60*60*1000, 730*60*60*1000, 2920*60*60*1000];
class CustomItem {
// Root variables
id;
last_reviewed_at = 0;
// Item main info. Should always contain at least:
// type (KanaVocabulary, Vocabulary, Kanji, Radical), category (Vocabulary, Kanji, Radical), srs_lvl, characters, meanings, aux_meanings
// Optional: meaning_expl, lvl
// Radicals: --
// Kanji: primary_reading_type, onyomi, kunyomi, nanori || reading_expl
// Vocabulary: readings, aux_readings || ctx_jp, ctx_en, reading_expl, kanji
// KanaVocabulary: || crx_jp, ctx_en
info;
constructor(id, info) {
this.id = id;
this.info = info;
this.last_reviewed_at = Date.now();
}
isReadyForReview(levelingType, level) { // levelingType: none, internal, wk
if(this.last_reviewed_at < Date.now() - srsGaps[this.info.srs_lvl] && this.info.srs_lvl > -1) { // TODO: Change SRS stage check to > 0 once lessons are implemented
if(this.info.srs_lvl > 0) return true; // If item is already in SRS, ignore levels
else if(levelingType == "none") return true;
else if(levelingType == "internal" && (!this.info.lvl || level >= this.info.lvl)) return true;
else if(levelingType == "wk" && (!this.info.lvl || CustomSRSSettings.userSettings.lastKnownLevel >= this.info.lvl)) return true;
}
return false;
}
getTimeUntilReview(levelingType, level) { // In hours, rounded to integer
if(this.isReadyForReview(levelingType, level)) {
return "Now";
} else {
if((levelingType == "internal" && this.info.lvl && level < this.info.lvl) || (levelingType == "wk" && this.info.lvl && CustomSRSSettings.userSettings.lastKnownLevel < this.info.lvl)) {
return "Locked";
} else return Math.round((srsGaps[this.info.srs_lvl] - (Date.now() - this.last_reviewed_at)) / (60*60*1000)) + "h";
}
}
incrementSRS() {
if(this.info.srs_lvl < 9) this.info.srs_lvl++;
this.last_reviewed_at = Date.now();
StorageManager.savePackProfile(activePackProfile, "main");
}
decrementSRS() {
if(this.info.srs_lvl > 1) {
if(this.info.srs_lvl < 5) this.info.srs_lvl--;
else this.info.srs_lvl -= 2;
}
this.last_reviewed_at = Date.now();
StorageManager.savePackProfile(activePackProfile, "main");
}
getSRS(packID) {
return [Utils.cantorNumber(packID, this.id), parseInt(this.info.srs_lvl)];
}
getQueueItem(packID) {
switch(this.info.type) {
case "Radical":
return {
id: Utils.cantorNumber(packID, this.id),
type: this.info.type,
subject_category: this.info.category,
characters: this.info.characters,
meanings: this.info.meanings,
auxiliary_meanings: this.info.aux_meanings || [],
kanji: this.info.kanji || []
};
case "Kanji":
return {
id: Utils.cantorNumber(packID, this.id),
type: this.info.type,
subject_category: this.info.category,
characters: this.info.characters,
meanings: this.info.meanings,
auxiliary_meanings: this.info.aux_meanings || [],
primary_reading_type: this.info.primary_reading_type,
onyomi: this.info.onyomi || [],
kunyomi: this.info.kunyomi || [],
nanori: this.info.nanori || [],
auxiliary_readings: this.info.aux_readings || [],
radicals: this.info.radicals || [],
vocabulary: this.info.vocabulary || []
};
case "Vocabulary":
return {
id: Utils.cantorNumber(packID, this.id),
type: this.info.type,
subject_category: this.info.category,
characters: this.info.characters,
meanings: this.info.meanings,
auxiliary_meanings: this.info.aux_meanings || [],
readings: this.info.readings.map(reading => ({"reading": reading, "pronunciations": []})),
auxiliary_readings: this.info.aux_readings || [],
kanji: this.info.kanji || []
};
case "KanaVocabulary":
return {
id: Utils.cantorNumber(packID, this.id),
type: this.info.type,
subject_category: this.info.category,
characters: this.info.characters,
meanings: this.info.meanings,
auxiliary_meanings: this.info.aux_meanings || [],
readings: this.info.readings.map(reading => ({"reading": reading, "pronunciations": []}))
};
}
}
static fromObject(object) {
let item;
if(!object.info) { // If item from before update TODO: remove after a few weeks
let newInfo = {};
newInfo.type = object.type;
newInfo.category = object.subject_category;
newInfo.srs_lvl = object.srs_stage;
newInfo.characters = object.characters;
newInfo.meanings = object.meanings;
if(object.readings) newInfo.readings = object.readings;
if(object.auxiliary_readings && object.auxiliary_readings.length > 0) newInfo.aux_readings = object.auxiliary_readings;
if(object.auxiliary_meanings && object.auxiliary_meanings.length > 0) newInfo.aux_meanings = object.auxiliary_meanings;
if(object.meaning_explanation) newInfo.meaning_expl = object.meaning_explanation;
if(object.reading_explanation && (object.type == "Vocabulary" || object.type == "Kanji")) newInfo.reading_expl = object.reading_explanation;
if(object.primary_reading_type) newInfo.primary_reading_type = object.primary_reading_type;
if(object.onyomi) newInfo.onyomi = object.onyomi;
if(object.kunyomi) newInfo.kunyomi = object.kunyomi;
if(object.nanori) newInfo.nanori = object.nanori;
item = new CustomItem(object.id, newInfo);
}
else item = new CustomItem(object.id, object.info);
if(item.info.context_sentences) { // Convert context_sentences to ctx_jp and ctx_en TODO: remove after a few weeks
for(let i = 0; i < item.info.context_sentences.length; i++) {
item.info.ctx_jp = [];
item.info.ctx_en = [];
if(i % 2 == 0) item.info.ctx_jp.push(item.info.context_sentences[i]);
else item.info.ctx_en.push(item.info.context_sentences[i]);
}
delete item.info.context_sentences;
}
item.last_reviewed_at = object.last_reviewed_at;
return item;
}
}
class CustomItemPack {
name;
author;
version;
items = [];
active = true;
nextID = 0;
lvlType = "none"; // "none", "internal", "wk"
lvl = 1;
constructor(name, author, version, lvlType, lvl = 1) {
this.name = name;
this.author = author;
this.version = version;
this.lvlType = lvlType;
this.lvl = lvl;
}
getItem(id) {
return this.items.find(item => item.id === id);
}
getItemID(itemType, itemChar) {
let item = this.items.find(item => item.info.characters === itemChar && item.info.type === itemType);
if(item) return item.id;
else return null;
}
addItem(itemInfo) {
let id = this.nextID++;
let item = new CustomItem(id, itemInfo);
this.items.push(item);
}
editItem(id, itemInfo) {
let item = this.getItem(id);
delete item.info;
item.info = itemInfo;
}
removeItem(position) {
this.items.splice(position, 1);
}
getActiveReviews(packID) { // Get all items that were last reviewed more than 24 hours ago
if(!this.active) return [];
return this.items.filter(item => item.isReadyForReview(this.lvlType, this.lvl)).map(item => item.getQueueItem(packID));
}
getActiveReviewsSRS(packID) {
if(!this.active) return [];
return this.items.filter(item => item.isReadyForReview(this.lvlType, this.lvl)).map(item => item.getSRS(packID));
}
getNumActiveReviews() {
if(!this.active) return 0;
let num = 0;
for(let item of this.items) {
if(item.isReadyForReview(this.lvlType, this.lvl)) num++;
}
return num;
}
getItemTimeUntilReview(itemIndex) {
return this.items[itemIndex].getTimeUntilReview(this.lvlType, this.lvl);
}
static fromObject(object) {
let pack = new CustomItemPack(object.name, object.author, object.version, (object.lvlType ? object.lvlType : "none"), (object.lvl ? object.lvl : 1)); // TODO: Remove lvlType and lvl checks after a few weeks
pack.items = object.items.map(item => CustomItem.fromObject(item));
pack.active = object.active;
pack.nextID = (object.nextID || pack.items.length); // If lastID is not present, use the length of the items array
return pack;
}
}
class CustomPackProfile {
customPacks = [];
getPack(id) {
return this.customPacks[id];
}
addPack(newPack) {
this.customPacks.push(newPack);
}
removePack(id) {
this.customPacks.splice(id, 1);
}
doesPackExist(packName, packAuthor, packVersion) {
for(let i = 0; i < this.customPacks.length; i++) {
let pack = this.customPacks[i];
if(pack.name === packName && pack.author === packAuthor) {
if(pack.version === packVersion) return "exists";
else return i;
}
}
return "no";
}
updatePack(id, newPack) { // Update pack but keeping the SRS stages of items that are in both the old and new pack
let oldPack = this.customPacks[id];
newPack = StorageManager.packFromJSON(newPack);
for(let i = 0; i < newPack.items.length; i++) {
let newItem = newPack.items[i];
let oldItem = oldPack.items.find(item => item.id === newItem.id);
if(oldItem) {
newItem.info.srs_lvl = oldItem.info.srs_lvl;
newItem.last_reviewed_at = oldItem.last_reviewed_at;
}
}
this.customPacks[id] = newPack;
}
getActiveReviews() {
let activeReviews = [];
for(let i = 0; i < this.customPacks.length; i++) {
activeReviews.push(...this.customPacks[i].getActiveReviews(i));
}
return activeReviews;
}
getNumActiveReviews() {
return this.customPacks.reduce((acc, pack) => acc + pack.getNumActiveReviews(), 0);
}
getActiveReviewsSRS() {
let activeReviewsSRS = [];
for(let i = 0; i < this.customPacks.length; i++) {
activeReviewsSRS.push(...this.customPacks[i].getActiveReviewsSRS(i));
}
return activeReviewsSRS;
}
getSubjectInfo(cantorNum) { // Get details of custom item for review page details display
let [packID, itemID] = Utils.reverseCantorNumber(cantorNum);
let item = this.getPack(packID).getItem(itemID);
return makeDetailsHTML(item);
}
submitReview(cantorNum, meaningIncorrectNum, readingIncorrectNum) {
let [packID, itemID] = Utils.reverseCantorNumber(cantorNum);
let item = this.customPacks[packID].getItem(itemID);
if(meaningIncorrectNum > 0 || readingIncorrectNum > 0) {
item.decrementSRS();
} else {
item.incrementSRS();
// Check if pack should level up
let pack = this.customPacks[packID];
if(pack.lvlType == "internal") {
for(let item of pack.items) {
if((!item.info.lvl || item.info.lvl <= pack.lvl) && item.info.srs_lvl < 5) break;
}
pack.lvl++;
}
}
}
static fromObject(object) {
let packProfile = new CustomPackProfile();
packProfile.customPacks = object.customPacks.map(pack => CustomItemPack.fromObject(pack));
return packProfile;
}
}
// ------------------- Utility classes -------------------
class Utils {
static cantorNumber(a, b) {
return -(0.5 * (a + b) * (a + b + 1) + b) - 1;
}
static reverseCantorNumber(z) {
z = -z - 1;
let w = Math.floor((Math.sqrt(8 * z + 1) - 1) / 2);
let y = z - ((w * w + w) / 2);
let x = w - y;
return [x, y];
}
static async get_controller(name) {
let controller;
while(!controller) {
try {
controller = Stimulus.getControllerForElementAndIdentifier(document.querySelector(`[data-controller~="${name}"]`),name);
} catch(e) {
console.log("Waiting for controller " + name);
}
await new Promise(r => setTimeout(r, 50));
}
return controller;
}
static async wkAPIRequest(endpoint, method = "GET", data = null) {
if(!CustomSRSSettings.userSettings.apiKey) console.error("CustomSRS: No API key set");
let url = "https://api.wanikani.com/v2/" + endpoint;
let headers = new Headers({
Authorization: "Bearer " + CustomSRSSettings.userSettings.apiKey,
});
let apiRequest = new Request(url, {
method: method,
headers: headers
});
if(data) apiRequest.body = JSON.stringify(data);
let response = await fetch(apiRequest);
return response.json();
}
}
class CustomSRSSettings {
static defaultUserSettings = {
showItemDueTime: true,
itemQueueMode: "start",
exportSRSData: false,
lastKnownLevel: 0,
apiKey: null
};
static userSettings = this.defaultUserSettings;
static savedData = {
capturedWKReview: null
};
static validateSettings() {
for(let setting in this.defaultUserSettings) {
if(this.userSettings[setting] === undefined) this.userSettings[setting] = this.defaultUserSettings[setting];
}
}
}
class StorageManager {
// Get custom packs saved in GM storage
static async loadPackProfile(profileName) {
let savedPackProfile = CustomPackProfile.fromObject(await GM.getValue("customPackProfile_" + profileName, new CustomPackProfile()));
return savedPackProfile;
}
// Save custom packs to GM storage
static async savePackProfile(packProfile, profileName) {
GM.setValue("customPackProfile_" + profileName, packProfile);
}
// Settings
static async saveSettings() {
GM.setValue("custom_srs_user_data", CustomSRSSettings.userSettings);
GM.setValue("custom_srs_saved_data", CustomSRSSettings.savedData);
}
static async loadSettings() {
CustomSRSSettings.userSettings = await GM.getValue("custom_srs_user_data", CustomSRSSettings.userSettings);
CustomSRSSettings.validateSettings();
CustomSRSSettings.savedData = await GM.getValue("custom_srs_saved_data", CustomSRSSettings.savedData);
}
static packFromJSON(json) {
let pack = CustomItemPack.fromObject(json);
return pack;
}
static packToJSON(pack) {
let packJSON = JSON.parse(JSON.stringify(pack));
if(!CustomSRSSettings.userSettings.exportSRSData) {
packJSON.items.forEach(item => {
item.last_reviewed_at = 0;
item.info.srs_lvl = 0;
});
}
return JSON.stringify(packJSON);
}
}
let activePackProfile = await StorageManager.loadPackProfile("main");
await StorageManager.loadSettings();
let quizStatsController;
// ----------- If on review page -----------
if (window.location.pathname.includes("/review")) {
if(activePackProfile.getNumActiveReviews() !== 0) {
// Add style to root to prevent header flash
let headerStyle = document.createElement("style");
headerStyle.innerHTML = `
.character-header__characters {
transition: opacity 0.15s;
}
.character-header__loading .character-header__characters {
opacity: 0;
}
`;
document.head.append(headerStyle);
}
// Add custom items to the quiz queue and update captured WK review
document.addEventListener("DOMContentLoaded", () => {
let changedFirstItem = false;
let queueEl = document.getElementById('quiz-queue');
let parentEl = queueEl.parentElement;
queueEl.remove();
let cloneEl = queueEl.cloneNode(true);
let queueElement = JSON.parse(cloneEl.querySelector("script[data-quiz-queue-target='subjects']").innerHTML);
let SRSElement = JSON.parse(cloneEl.querySelector("script[data-quiz-queue-target='subjectIdsWithSRS']").innerHTML);
// Remove captured WK review from queue
if(queueElement.length === 1 || (CustomSRSSettings.savedData.capturedWKReview && queueElement[1].id === CustomSRSSettings.savedData.capturedWKReview.id)) {
CustomSRSSettings.savedData.capturedWKReview = queueElement.shift();
SRSElement.shift();
changedFirstItem = true;
console.log("CustomSRS: Captured first item from queue.");
} else {
CustomSRSSettings.savedData.capturedWKReview = queueElement[1];
queueElement.splice(1, 1);
SRSElement.splice(1, 1);
console.log("CustomSRS: Captured second item from queue.");
}
// Add custom items to queue
if(activePackProfile.getNumActiveReviews() !== 0) {
switch(CustomSRSSettings.userSettings.itemQueueMode) {
case "weighted-start":
let reviewsToAddW = activePackProfile.getActiveReviews();
let reviewsSRSToAddW = activePackProfile.getActiveReviewsSRS();
for(let i = 0; i < reviewsToAddW.length; i++) {
let pos = Math.floor(Math.random() * queueElement.length / 4);
if(pos === 0) changedFirstItem = true;
queueElement.splice(pos, 0, reviewsToAddW[i]);
SRSElement.splice(pos, 0, reviewsSRSToAddW[i]);
}
break;
case "random":
let reviewsToAdd = activePackProfile.getActiveReviews();
let reviewsSRSToAdd = activePackProfile.getActiveReviewsSRS();
for(let i = 0; i < reviewsToAdd.length; i++) {
let pos = Math.floor(Math.random() * queueElement.length);
if(pos === 0) changedFirstItem = true;
queueElement.splice(pos, 0, reviewsToAdd[i]);
SRSElement.splice(pos, 0, reviewsSRSToAdd[i]);
}
break;
case "start":
changedFirstItem = true;
queueElement = activePackProfile.getActiveReviews().concat(queueElement);
SRSElement = activePackProfile.getActiveReviewsSRS().concat(SRSElement);
break;
}
}
cloneEl.querySelector("script[data-quiz-queue-target='subjects']").innerHTML = JSON.stringify(queueElement);
cloneEl.querySelector("script[data-quiz-queue-target='subjectIdsWithSRS']").innerHTML = JSON.stringify(SRSElement);
parentEl.appendChild(cloneEl);
StorageManager.saveSettings();
if(changedFirstItem) {
let headerElement = document.querySelector(".character-header");
headerElement.classList.add("character-header__loading");
for(let className of headerElement.classList) { // Fix header colour issues
if(className.includes("character-header--")) {
headerElement.classList.remove(className);
headerElement.classList.add("character-header--" + activePackProfile.getActiveReviews()[0].subject_category.toLowerCase());
setTimeout(() => {
headerElement.classList.remove("character-header__loading");
}, 500);
break;
}
}
}
loadControllers();
});
// Catch submission fetch and stop it if submitted item is a custom item
const { fetch: originalFetch } = unsafeWindow;
unsafeWindow.fetch = async (...args) => {
let [resource, config] = args;
if (resource.includes("/subjects/review") && config != null && config.method === "POST") {
let payload = JSON.parse(config.body);
// Check if submitted item is a custom item
if(payload.counts && payload.counts[0].id < 0) {
// Update custom item SRS
activePackProfile.submitReview(payload.counts[0].id, payload.counts[0].meaning, payload.counts[0].reading);
return new Response("{}", { status: 200 });
} else {
if(payload.counts[0].id == CustomSRSSettings.savedData.capturedWKReview.id) { // Check if somehow the captured WK review is being submitted
CustomSRSSettings.savedData.capturedWKReview = null;
StorageManager.saveSettings();
}
return originalFetch(...args);
}
// Catch subject info fetch and return custom item details if the number at the end of the url is negative
} else if (resource.includes("/subject_info/") && config && config.method === "get" && resource.split("/").pop() < 0) {
// Submit original fetch but to different URL to get usable headers
args[0] = "https://www.wanikani.com/subject_info/1";
let response = await originalFetch(...args);
let subjectId = resource.split("/").pop();
let subjectInfo = activePackProfile.getSubjectInfo(subjectId);
return new Response(subjectInfo, {
status: response.status,
headers: response.headers
});
} else {
return originalFetch(...args);
}
};
// ----------- If on lessons page -----------
} else if (window.location.pathname.includes("/lessons")) {
// TODO
// ----------- If on dashboard page -----------
} else if (window.location.pathname.includes("/dashboard") || window.location.pathname === "/") {
// Catch lesson / review count fetch and update it with custom item count
const { fetch: originalFetch } = unsafeWindow;
unsafeWindow.fetch = async (...args) => {
let [resource, config] = args;
if (resource.includes("lesson-and-review-count") && config != null && config.method === "get") {
let response = await originalFetch(...args);
let data = await response.text();
let res = new Response(updateLessonReviewCountData(data), {
status: response.status,
headers: response.headers
});
return res;
} else {
return originalFetch(...args);
}
};
// Catch document load to edit review count on dashboard
document.addEventListener("DOMContentLoaded", () => {
let reviewNumberElement = document.querySelector(".reviews-dashboard .reviews-dashboard__count-text span");
reviewNumberElement.innerHTML = parseInt(reviewNumberElement.innerHTML) + activePackProfile.getNumActiveReviews() + (CustomSRSSettings.savedData.capturedWKReview ? -1 : 0);
console.log("Captured review item: " + (CustomSRSSettings.savedData.capturedWKReview ? CustomSRSSettings.savedData.capturedWKReview.id : "none"));
let reviewTile = document.querySelector("div.reviews-dashboard");
if(reviewTile.querySelector(".reviews-dashboard__buttons") === null && activePackProfile.getNumActiveReviews() > 0) { // If failed to catch WK review and custom items are due, display error message
reviewTile.querySelector(".reviews-dashboard__text .wk-text").innerHTML = "CustomSRS Error. Please wait for WK review item to be available.";
} else if(parseInt(reviewTile.querySelector(".count-bubble").innerHTML) === 0) { // If no custom items are due, update review tile to remove buttons
reviewTile.querySelector(".reviews-dashboard__buttons").remove();
reviewTile.classList.add("reviews-dashboard--complete");
reviewTile.querySelector(".reviews-dashboard__text .wk-text").innerHTML = "There are no more reviews to do right now.";
}
});
// Update the stored user level
let response = await Utils.wkAPIRequest("user");
if(response && response.data && response.data.level) {
CustomSRSSettings.userSettings.lastKnownLevel = response.data.level;
StorageManager.saveSettings();
}
} else {
// Catch lesson / review count fetch and update it with custom item count
const { fetch: originalFetch } = unsafeWindow;
unsafeWindow.fetch = async (...args) => {
let [resource, config] = args;
if (resource.includes("lesson-and-review-count") && config != null && config.method === "get") {
let response = await originalFetch(...args);
let data = await response.text();
let res = new Response(updateLessonReviewCountData(data), {
status: response.status,
headers: response.headers
});
return res;
} else {
return originalFetch(...args);
}
};
}
// ----------- UTILITIES -----------
function parseHTML(html) {
var t = document.createElement('template');
t.innerHTML = html;
return t.content;
}
function updateLessonReviewCountData(data) {
data = parseHTML(data);
let reviewCountElement = data.querySelector("a[href='/subjects/review'] .lesson-and-review-count__count");
// If reviewCountElement is null, replace the span .lesson-and-review-count__item with some custom HTML
let numActiveReviews = activePackProfile.getNumActiveReviews();
if(reviewCountElement === null && numActiveReviews > 0) {
let reviewTile = data.querySelector(".lesson-and-review-count__item:nth-child(2)");
reviewTile.outerHTML = `
<a class="lesson-and-review-count__item" target="_top" href="/subjects/review">
<div class="lesson-and-review-count__count">${numActiveReviews}</div>
<div class="lesson-and-review-count__label">Reviews</div>
</a>
`;
} else {
if(numActiveReviews > 0 || (!CustomSRSSettings.savedData.capturedWKReview && parseInt(reviewCountElement.innerHTML) > 0) || parseInt(reviewCountElement.innerHTML) > 1) reviewCountElement.innerHTML = parseInt(reviewCountElement.innerHTML) + numActiveReviews + (CustomSRSSettings.savedData.capturedWKReview ? -1 : 0);
else {
let reviewTile = data.querySelector(".lesson-and-review-count__item:nth-child(2)");
reviewTile.outerHTML = `
<span class="lesson-and-review-count__item" target="_top">
<div class="lesson-and-review-count__count lesson-and-review-count__count--zero">0</div>
<div class="lesson-and-review-count__label">Reviews</div>
</span>
`;
}
}
// Convert the DocumentFragment back to a string and return it as a Response
return (new XMLSerializer()).serializeToString(data);
}
async function loadControllers() {
quizStatsController = await Utils.get_controller('quiz-statistics');
quizStatsController.remainingCountTarget.innerText = parseInt(quizStatsController.remainingCountTarget.innerText) + activePackProfile.getNumActiveReviews() + (CustomSRSSettings.savedData.capturedWKReview ? -1 : 0);
}
})();