您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays the history of answers for each item in review sessions
// ==UserScript== // @name WaniKani Review Answer History // @namespace http://tampermonkey.net/ // @version 1.5 // @description Displays the history of answers for each item in review sessions // @author Wantitled // @match https://www.wanikani.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=wanikani.com // @grant none // @license MIT // ==/UserScript== // Checks for Wanikani Open Framework if (!window.wkof){ if( confirm(` WaniKani Review Answer History requires Wanikani Open Framework. Click "OK" to be forwarded to installation instructions.`)){ window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549'} return; } // Inits let WKAnswerHistory var observer; var input; var itemElem; const url = window.location.href; let item, items_by_id; let bySlug; let pageItem, itemType; // Item data only needed for review sessions wkof.include('ItemData'); wkof.ready('ItemData').then(get_history).then(get_items).then(check_history_data).then(initiate); async function check_history_data() { if (WKAnswerHistory["radicals"]){ console.log("Old item type name found, renaming to 'radical'."); WKAnswerHistory["radical"] = WKAnswerHistory["radicals"]; delete WKAnswerHistory["radicals"]; wkof.file_cache.save("WKAnswerHistory", WKAnswerHistory); } if ((/\D/.test(Object.keys(WKAnswerHistory["radical"])[0]) && Object.keys(WKAnswerHistory["radical"]).length > 0 ) || (/\D/.test(Object.keys(WKAnswerHistory["kanji"])[0]) && Object.keys(WKAnswerHistory["kanji"]).length > 0 ) || (/\D/.test(Object.keys(WKAnswerHistory["vocabulary"])[0]) && Object.keys(WKAnswerHistory["vocabulary"]).length > 0)){ alert("Old item name(s) found, please wait a moment..."); wkof.file_cache.save("WKAnswerHistory_backup", WKAnswerHistory); bySlug = wkof.ItemData.get_index(item, "slug"); for (let item_type in WKAnswerHistory){ for (let item_slug in WKAnswerHistory[item_type]){ if (/\D/.test(item_slug)){ let item_id = get_id(item_slug, item_type); WKAnswerHistory[item_type][item_id] = WKAnswerHistory[item_type][item_slug]; delete WKAnswerHistory[item_type][item_slug]; } } } wkof.file_cache.save("WKAnswerHistory", WKAnswerHistory); alert("Items converted!"); } } const get_id = (slug, type) => { const matches = bySlug[slug]; if (Array.isArray(matches)) { return matches.find(m => m.object === type)?.id; } else { return matches?.id; } } // Gets the answer history async function get_history () { if (!wkof.file_cache.dir["WKAnswerHistory"]){ WKAnswerHistory = { "radical": {}, "kanji": {}, "vocabulary": {} }; if (confirm("Wanikani Review Answer History has not detected any review answer data. If this is your first time using the script, click OK. If this is a mistake, please cancel.")){ wkof.file_cache.save("WKAnswerHistory", WKAnswerHistory); } } else { WKAnswerHistory = await wkof.file_cache.load("WKAnswerHistory"); } } // Gets item data async function get_items() { item = await wkof.ItemData.get_items('assignments'); items_by_id = wkof.ItemData.get_index(item, 'subject_id'); } // Adds the input to the item history object const save_data = (answer, itemType, item, status, language, override) => { if (!WKAnswerHistory[itemType][item]){ WKAnswerHistory[itemType][item] = { "answers": [], "timestamps": [], "itemStatus": [], "SRSLevel": [], "language": [] } } if (!override) { let lang; if (language === null){ lang = "en"; } else { lang = "ja"; } WKAnswerHistory[itemType][item].answers.push(answer); WKAnswerHistory[itemType][item].timestamps.push(getTimestamp()); WKAnswerHistory[itemType][item].itemStatus.push(status); WKAnswerHistory[itemType][item].SRSLevel.push(returnItemInfo()); WKAnswerHistory[itemType][item].language.push(lang); } else { WKAnswerHistory[itemType][item].itemStatus[WKAnswerHistory[itemType][item].itemStatus.length - 1] = status; } wkof.file_cache.save("WKAnswerHistory", WKAnswerHistory); } // Gets time and date of review (seconds are included as the same item can be reviewed a few seconds apart) const getTimestamp = () => { let time = new Date(); let addZero = (num) => { if (String(num).length === 1){ num = "0" + String(num); } return num; } let year = time.getFullYear(); let month = addZero(parseInt(time.getMonth()) + 1); let day = addZero(time.getDate()); let hours = time.getHours(); let minutes = addZero(time.getMinutes()); let seconds = addZero(time.getSeconds()) return year + "/" + month + "/" + day + ", " + hours + ":" + minutes + ":" + seconds ; } // Gets current item data const returnItemInfo = () => { let review_item = $.jStorage.get('currentItem'); let item = items_by_id[review_item.id]; return getSRSLevel(item) } const additional_info = (item, info) => { let item_obj = items_by_id[item]; switch (info){ case "unlock": return item_obj?.assignments?.unlocked_at;break; case "lesson": return item_obj?.assignments?.started_at;break; case "guru": return item_obj?.assignments?.passed_at;break; case "burn": return item_obj?.assignments?.burned_at;break; case "resurrect": return item_obj?.assignments?.resurrected_at;break; } } // Gets the current item's SRS level (the level the item is being reviewed at) const getSRSLevel = (wkof_item) => {return wkof_item?.assignments?.srs_stage ?? -1} // Status checker checks for a class change on the input field which signifies an answer or an override const statusChecker = (fieldsetElem, lastClass) => { observer = new MutationObserver((mutationsList) => { let itemType = itemElem.classList[0]; let item = $.jStorage.get('currentItem').id; let answer = input.value; let lang = input.getAttribute("lang"); for(let mutation of mutationsList) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { let currentClass = mutation.target.classList[0]; if (currentClass){ if (lastClass === undefined) { lastClass = currentClass switch (currentClass){ case "correct": save_data(answer, itemType, item, currentClass, lang, false);break; case "incorrect": save_data(answer, itemType, item, currentClass, lang, false);break; } } else { switch (currentClass){ case "correct": save_data(answer, itemType, item, currentClass, lang, true);break; case "incorrect": save_data(answer, itemType, item, currentClass, lang, true);break; case "WKO_ignored": save_data(answer, itemType, item, currentClass, lang, true);break; default: break; } } } else {lastClass = currentClass} }} }); observer.observe(fieldsetElem, {attributes: true}); } // Creates the table headers for the table on the item page const createHeaders = () => { let headers = document.createElement("tr"); const reviewTypeHeader = document.createElement("th"); reviewTypeHeader.innerText = "Review Type"; const answerHeader = document.createElement("th"); answerHeader.innerText = "Answer"; const srsHeader = document.createElement("th"); srsHeader.innerText = "SRS Level"; const timestampHeader = document.createElement("th"); timestampHeader.innerText = "Date Answered"; headers.appendChild(reviewTypeHeader); headers.appendChild(answerHeader); headers.appendChild(srsHeader); headers.appendChild(timestampHeader); return headers; } // Creates the table rows and inserts the data for the item page table const buildTable = (itemObj, tbody) => {; let milestones = get_milestones(pageItem); console.log(milestones); for (let i = itemObj.answers.length - 1; i >= 0; i--){ for (let j = milestones.milestone_time.length - 1; j >= 0; j--){ let time_obj = new Date(milestones.milestone_time[j]); if (convertTime(itemObj.timestamps[i]).getTime() < time_obj.getTime()){ let milestone_tr = construct_milestone(milestones.milestone_name[j], milestones.milestone_time[j]); tbody.appendChild(milestone_tr); milestones.milestone_name.pop(); milestones.milestone_time.pop(); } } let tr = document.createElement("tr"); tr.style.backgroundColor = getColor(itemObj.itemStatus[i]); tr.style.color = "#FFF"; let ans_td = document.createElement("td"); ans_td.innerText = itemObj.answers[i]; if (itemObj.language[i] === "ja"){ans_td.setAttribute("lang", "ja");} ans_td.style.textAlign = "center"; let srs_td = document.createElement("td"); srs_td.innerText = srs(itemObj.SRSLevel[i]) + " Level"; srs_td.style.textAlign = "center"; let date_td = document.createElement("td"); date_td.innerText = fix_date(convertTime(itemObj.timestamps[i])); date_td.style.textAlign = "center"; let lang_td = document.createElement("td"); if (itemType === "radical"){ lang_td.innerText = "Name"; } else { lang_td.innerText = reviewType(itemObj.language[i]); } lang_td.style.textAlign = "center"; tr.appendChild(lang_td); tr.appendChild(ans_td); tr.appendChild(srs_td); tr.appendChild(date_td); tbody.appendChild(tr); if (i === 0){ for (let j = milestones.milestone_time.length - 1; j >= 0; j--){ let time_obj = new Date(milestones.milestone_time[j]); let milestone_tr = construct_milestone(milestones.milestone_name[j], milestones.milestone_time[j]); tbody.appendChild(milestone_tr); } } } } // Converts the answer status to the corresponding color const getColor = (status) => { switch (status) { case "correct": return "#88CC00";break; case "incorrect": return "#FF0033"; break; case "WKO_ignored": return "#FFCC00"; break; } } // Converts the language of the item to get the review type const reviewType = (lang) => { if (lang === "ja"){return "Reading"} else {return "Meaning"} } const convertTime = (time) => { let first_half = time.substr(0,time.indexOf(",")); let second_half = time.substr(time.indexOf(" ") + 1, time.length); first_half = first_half.replaceAll("/", "-"); let converted_time = new Date(first_half + "T" + second_half); return converted_time; } // Gets the srs level from the srs value const srs = (srsKey) => { switch (srsKey){ case -1: return "Missing";break; case 1: return "Apprentice 1";break; case 2: return "Apprentice 2";break; case 3: return "Apprentice 3";break; case 4: return "Apprentice 4";break; case 5: return "Guru 1";break; case 6: return "Guru 2";break; case 7: return "Master";break; case 8: return "Enlightened";break; default: return "";break; } } const get_milestones = (item_id) => { let milestonesArr = ["unlock", "lesson", "guru", "burn", "resurrect"]; let milestones = {"milestone_name": [], "milestone_time": [],} for (let i = 0; i < milestonesArr.length; i++){ let milestone_time = additional_info(item_id, milestonesArr[i]); if (milestone_time !== null){ milestones.milestone_name.push(milestonesArr[i]); milestones.milestone_time.push(milestone_time); } } return milestones; } const construct_milestone = (milestone, time) => { let milestone_tr = document.createElement("tr"); milestone_tr.style.backgroundColor = milestone_color(milestone) milestone_tr.style.lineHeight = "1.2rem"; milestone_tr.style.color = "#FFF"; milestone_tr.style.fontWeight = "500"; let type_td = document.createElement("td"); type_td.innerText = "Milestone"; type_td.style.textAlign = "center"; let milestone_td = document.createElement("td"); milestone_td.innerText = milestone_text(milestone); milestone_td.style.textAlign = "center"; let time_td = document.createElement("td"); let time_obj = new Date(time); time_td.innerText = fix_date(time_obj); time_td.style.textAlign = "center"; milestone_tr.appendChild(type_td); milestone_tr.appendChild(document.createElement("td")); milestone_tr.appendChild(milestone_td); milestone_tr.appendChild(time_td); return milestone_tr; } const fix_date = (time) => { let first_half = time.toDateString(); let second_half = time.toLocaleTimeString() return first_half + ", " + second_half; } const milestone_color = (milestone) => { switch (milestone){ case "unlock": return "#A4A4A4";break; case "lesson": return "#F400A2";break; case "guru": return "#9D34B7";break; case "burn": return "#4F4F4F";break; case "resurrect": return "#FAAF0E";break; } } const milestone_text = (milestone) => { switch (milestone){ case "unlock": return "Unlocked";break; case "lesson": return "Completed Lesson";break; case "guru": return "First Guru";break; case "burn": return "Burned";break; case "resurrect": return "Resurrected";break; } } function initiate() { 'use strict'; // Checks for the review page to collect answer data if (/\/review\/session+/.test(url)){ input = document.getElementById("user-response"); itemElem = document.getElementById("character"); let fieldsetElem = input.parentElement; let lastClass = fieldsetElem.classList[0]; statusChecker(fieldsetElem, lastClass); } // Checks for an item info page to display the data if (/\/radicals\/+/.test(url) || /\/kanji\/+/.test(url) || /\/vocabulary\/+/.test(url)){ // Gets the page's item pageItem = parseInt(document.querySelector('meta[name=subject_id]').content); itemType = url.substring(url.indexOf("wanikani.com/") + 13, url.lastIndexOf("/")); if (itemType === "radicals"){itemType = "radical"}; // Adds navigation button to top bar const history_li = document.createElement("li"); const history_a = document.createElement("a"); history_a.innerText = "Review History"; history_a.setAttribute("href", "#history"); history_li.appendChild(history_a); if (document.querySelector("[href='#progress']")){ let progress_li = document.querySelector("[href='#progress']").parentNode; document.querySelector(".page-list-header").parentNode.insertBefore(history_li, progress_li); } else { document.querySelector(".page-list-header").parentNode.appendChild(history_li); } // Section element for the review history const reviewHistorySection = document.createElement("section"); reviewHistorySection.setAttribute("id", "history"); reviewHistorySection.style.fontFamily = '"Ubuntu", Helvetica, Arial, sans-serif'; reviewHistorySection.style.fontSize = "16px"; // Title for the section const sectionHead = document.createElement("h2"); sectionHead.innerText = "Review History"; // Table element const table = document.createElement("table"); table.style.width = "100%"; table.style.lineHeight = "1.5rem"; // Body section of the table const tbody = document.createElement("tbody"); // Headers for the table let headers = createHeaders(); headers.style.position = "sticky"; headers.style.top = "0"; headers.style.backgroundColor = "#eee"; headers.style.boxShadow = "0 2px 2px -1px rgba(0, 0, 0, 0.2)"; table.appendChild(headers); // Table wrapped in a div to allow scrolling when the table is taller than 500px const tableDiv = document.createElement("div"); // Displays the table if item data is found, otherwise displays a messsage if (WKAnswerHistory[itemType][pageItem]){ buildTable(WKAnswerHistory[itemType][pageItem], tbody); table.appendChild(tbody); tableDiv.style.maxHeight = "500px"; tableDiv.style.display = "block"; tableDiv.style.overflowY = "auto"; tableDiv.appendChild(table); } else { tableDiv.innerText = ("No answers have been recorded for this item yet."); tableDiv.style.color = "#666"; } reviewHistorySection.appendChild(sectionHead); reviewHistorySection.appendChild(tableDiv); if (itemType === "radical"){ document.getElementById("information").parentNode.appendChild(reviewHistorySection); } else { document.getElementById("meaning").parentNode.appendChild(reviewHistorySection); } } };