您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds an option to add and review your own custom vocabulary
当前为
// ==UserScript== // @name Wanikani Self-Study Plus // @namespace wkselfstudyplus // @description Adds an option to add and review your own custom vocabulary // @include *.wanikani.com/* // @exclude *.wanikani.com // @include *.wanikani.com/dashboard* // @include *.wanikani.com/community* // @version 0.1.2 // @author shudouken and Ethan // require https://raw.github.com/WaniKani/WanaKana/master/lib/wanakana.min.js // @grant none // ==/UserScript== /* * This script is licensed under the Creative Commons License * "Attribution-NonCommercial 3.0 Unported" * * More information at: * http://creativecommons.org/licenses/by-nc/3.0/ */ //shut up JSHint /* jshint multistr: true , jquery: true, indent:2 */ /* global window, wanakana, Storage, XDomainRequest */ // User Variables var APIkey = "YOUR_API_HERE"; var locksOn = true; //Disable vocab locks (unlocked items persist until deleted) var lockDB = true; //Set to false to unlock Kanji is not returned by the WaniKani database (ie. not returned by API) var reverse = true; //Include English to ひらがな reading reviews /* * Debugging */ var debugging = false; var scriptLog = debugging ? function (msg) { if (typeof msg === 'string') { window.console.log("WKSS: " + msg); } else { window.console.log(msg); } } : function () { }; /* * Settings and constants */ ///############################################### // Config for window sizes in pixels // add Window, standard 300 x 300 var addWindowHeight = 300; var addWindowWidth = 300; // export and import Window, standard 275 x 390 var exportImportWindowHeight = 275; var exportImportWindowWidth = 390; // edit Window, standard 380 x 800 var editWindowHeight = 380; var editWindowWidth = 800; // study(review) Window, standard 380 x 600 var studyWindowHeight = 380; var studyWindowWidth = 600; // result Window, standard 500 x 700 var resultWindowHeight = 500; var resultWindowWidth = 700; ///############################################### var errorAllowance = 4; //every x letters, you can make one mistake when entering the meaning //srs 4h, 8h, 24h, 3d (guru), 1w, 2w (master), 1m (enlightened), 4m (burned) var srslevels = []; srslevels.push("Apprentice"); srslevels.push("Apprentice"); srslevels.push("Apprentice"); srslevels.push("Apprentice"); srslevels.push("Guru"); srslevels.push("Guru"); srslevels.push("Master"); srslevels.push("Enlightened"); srslevels.push("Burned"); var srsintervals = []; var hrs = 60*60*1000; var days = 24*hrs; var weeks = 7*days; srsintervals.push(0); srsintervals.push(4*hrs); srsintervals.push(8*hrs); srsintervals.push(1*days); srsintervals.push(3*days); srsintervals.push(1*weeks); srsintervals.push(2*weeks); srsintervals.push(730*hrs);//average month srsintervals.push(2922*hrs);//average 4 months //GM_addStyle shim for compatibility function GM_addStyle(CssString){ //get DOM head var head = document.getElementsByTagName('head')[0]; if (head) { //build style tag var style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.textContent = CssString; //insert DOM style into head head.appendChild(style); } } /* * JQuery fixes */ $("[placeholder]").focus(function () { var input = $(this); if (input.val() == input.attr("placeholder")) { input.val("''"); input.removeClass("'placeholder'"); } }).blur(function () { var input = $(this); if (input.val() == "''" || input.val() == input.attr("placeholder")) { input.addClass("placeholder"); input.val(input.attr("placeholder")); } }).blur(); $("[placeholder]").parents("form").submit(function () { $(this).find("[placeholder]").each(function () { var input = $(this); if (input.val() == input.attr("placeholder")) { input.val(""); } }); }); /* * populate reviews when menu button pressed */ window.generateReviewList = function() { //if menu is invisible, it is about to be visible if ( $("#WKSS_dropdown").is(":hidden") ){ //This is really the only time it needs to run //unless we want to start updating in realtime by keeping track of the soonest item generateReviewList(); } }; /* * Add Item */ // event function to open "add window" and close any other window that might be open at the time. window.WKSS_add = function () { //show the add window $("#add").show(); //hide other windows $("#export").hide(); $("#import").hide(); $("#edit").hide(); $("#selfstudy").hide(); }; //'add window' html text var addHtml = '\n\ <div id="add" class="WKSS">\n\ <form id="addForm">\n\ <button id="AddCloseBtn" class="wkss-close" type="reset"><i class="icon-remove"></i></button>\n\ <h1>Add a new Item</h1>\n\ <input type="text" id="addKanji" placeholder="Enter 漢字, ひらがな or カタカナ">\n\ <input type="text" id="addReading" title="Leave empty to add vocabulary like する (to do)" placeholder="Enter reading">\n\ <input type="text" id="addMeaning" placeholder="Enter meaning">\n\ \n\ <p id="addStatus">Ready to add..</p>\n\ <button id="AddItemBtn" type="button">Add new Item</button>\n\ </form>\n\ </div>\n'; //add html to page source $("body").append(addHtml); //hide add window ("div add" code that was just appended) $("#add").hide(); function handleAddClick(){ var kanji = $("#addKanji").val().toLowerCase(); var reading = $("#addReading").val().toLowerCase().split(/[,、]+\s*/); //split at , or 、followed by 0 or any number of spaces var meaning = $("#addMeaning").val().toLowerCase().split(/[,、]+\s*/); var success = false; //initalise values var meanlen = 0; var i = meaning.length; while (i--){ meanlen += meaning[i].length; } //input is invalid: prompt user for valid input var item = {}; if (kanji.length === 0 || meanlen === 0) { $("#addStatus").text("One or more required fields are empty!"); if (kanji.length === 0) { $("#addKanji").addClass("error"); } else { $("#addKanji").removeClass("error"); } if (meanlen === 0) { $("#addMeaning").addClass("error"); } else { $("#addMeaning").removeClass("error"); } } else { scriptLog("building item: "+kanji); item.kanji = kanji; item.reading = reading; //optional item.meaning = meaning; //preserve data integrity by combining item and srsitem item.level = 0; item.date = Date.now(); item.manualLock = ""; item = setLocks(item); success = true; scriptLog("item is valid"); } //on successful creation of item if (success) { //clear error layout to required fields $("#addKanji").removeClass("error"); $("#addMeaning").removeClass("error"); //if there are already user items, retrieve vocabList // var vocabList = []; var vocabList = getFullList(); //check stored user items for duplicates ****************** to do: option for editing duplicate item with new input if(checkForDuplicates(vocabList,item)) { $("#addStatus").text("Duplicate Item detected!"); $("#addKanji").addClass("error"); return; } setVocItem(item); scriptLog("clear form"); $("#addForm")[0].reset(); //-------------------------------------------------------------------------------------------------------- if (item.manualLock === "yes" || item.manualLock === "DB" && lockDB){ $("#addStatus").html("<i class=\"icon-lock\"></i> Added locked item"); } else { $("#addStatus").html("<i class=\"icon-unlock\"></i>Added successfully"); } //-------------------------------------------------------------------------------------------------------- } } //function to fire on click event for "Add new Item" $("#AddItemBtn").click(function () { handleAddClick(); }); $("#AddCloseBtn").click(function () { $("#add").hide(); $("#addForm")[0].reset(); $("#addStatus").text('Ready to add..'); $("#addKanji").removeClass("error"); $("#addMeaning").removeClass("error"); }); //---Function wrappers to facilitate use of one localstorage array //---Maintains data integrity between previously two (vocab and srs) function getSrsItem(index){ var srsList = jQuery.parseJSON(localStorage.getItem('User-Vocab')); if(srsList){ var srsitem = srsList[index]; srsitem.i = index; //set index for item (->out) return srsitem; } }// deleting vocab properties should be unecessary function getSrsList(){ var srsList = jQuery.parseJSON(localStorage.getItem('User-Vocab')); if (!srsList){ srsList = jQuery.parseJSON(localStorage.getItem('User-SRS')); } var i = srsList.length; while(i--){ srsList[i].i=i; } //scriptLog("getSrsList: "+JSON.stringify(srsList)); return srsList; } function setSrsItem(srsitem){ var index = srsitem.i; scriptLog("srsitem:"); delete srsitem.i; // delete index (in->) //Pull out list (whole thing) var srsList = jQuery.parseJSON(localStorage.getItem('User-Vocab')); if(srsList){ if(srsList[index].kanji===srsitem.kanji){// try search by index scriptLog("success: "+srsitem.kanji+" found at index "+ index); //replace only the srs parts of the item srsList[index].date = srsitem.date; srsList[index].level = srsitem.level; srsList[index].locked = srsitem.locked; srsList[index].manualLock = srsitem.manualLock; }else{ //backup plan (cycle through list?) scriptLog("SRS Kanji not found in vocablist, needs work"); } scriptLog("item: "+JSON.stringify(srsitem)); localStorage.setItem('User-Vocab', JSON.stringify(srsList)); } } function getVocList(){ var vocList = jQuery.parseJSON(localStorage.getItem('User-Vocab')); if (vocList){ var v=vocList.length; while(v--){ vocList[v].i = v; //set index for item (->out) delete vocList[v].date; delete vocList[v].level; delete vocList[v].locked; delete vocList[v].manualLock; } }else{ //return empty if null vocList = []; } scriptLog("getVocList: "+JSON.stringify(vocList)); return vocList; } function setVocItem(item){ //Assumption: item comes only with kanji, reading and meaning delete item.i; item.level = 0; item.date = Date.now(); item.manualLock = ""; item = setLocks(item); var found = false; var vocList = jQuery.parseJSON(localStorage.getItem('User-Vocab')); if (vocList){ var v = vocList.length; while(v--){ if (vocList[v].kanji === item.kanji){ found = true; scriptLog("duplicate found, skipping item (give options in future)"); //add meaning and reading to existing item // vocList[v].meaning = item.meaning; // vocList[v].reading = item.reading; } } if (!found) { //provide index for faster searches scriptLog(item.kanji +" not found in vocablist, adding now"); scriptLog("Setting i to "+v); vocList.push(item); localStorage.setItem('User-Vocab',JSON.stringify(vocList)); } } else{ scriptLog("vocablist not found, creating now"); vocList = []; vocList.push(item); scriptLog("putting vocList in storage"); localStorage.setItem('User-Vocab', JSON.stringify(vocList)); } } function convertToArray(){ //quick solution to convert "reading 1, reading2" to ["reading 1", "reading 2"] var fullList = jQuery.parseJSON(localStorage.getItem('User-Vocab')); var f = fullList.length; while (f--){ if (typeof fullList[f].reading == 'string'){ fullList[f].reading = fullList[f].reading.split(","); } if (typeof fullList[f].meaning == 'string'){ fullList[f].meaning = fullList[f].meaning.split(","); } } localStorage.setItem('User-Vocab', JSON.stringify(fullList)); } function getFullList(){ var fullList = jQuery.parseJSON(localStorage.getItem('User-Vocab')); if(!fullList){ fullList=[]; } return fullList; } //checks if an item is present in a list function checkForDuplicates(list, item) { scriptLog("Check for dupes with:" + item.kanji); var i = list.length; while(i--){ list[i].i = i; //set index property for quick lookup if(list[i].kanji == item.kanji) return true; } return false; } //manages .locked property of srsitem /*This function manages the .locked and manualLock properties of srsitem .locked is a real time evaluation of the item (is any of the kanji in the word locked?) .manualLock will return 'no' if .locked has ever returned 'no'. This is to stop items being locked again after they have been unlocked if any of the kanji used falls below the unlock threshold (eg. if the 勉 in 勉強 falls back to apprentice, we do not want to lock up 勉強 again.) */ function setLocks(srsitem){ //functions: // isKanjiLocked(srsitem) scriptLog("isKanjiLocked1"); //-----------------------] scriptLog("var kanjiList=localStorage.getItem('User-KanjiList');"); var kanjiList = localStorage.getItem('User-KanjiList'); scriptLog(JSON.stringify(kanjiList)); scriptLog("var listObject=JSON.parse(kanjiList);"); var listObject=JSON.parse(kanjiList); scriptLog(listObject); //var = jQuery.parseJSON(localStorage.getItem('User-KanjiList')); //-----------------------] srsitem.locked = isKanjiLocked(srsitem, listObject); //once manualLock is "no" it stays "no" //this is to stop an item from locking up again if //any of the component kanji fall below 'guru' if (srsitem.manualLock !== "no"){ srsitem.manualLock = srsitem.locked; } scriptLog("setting locks for "+ srsitem.kanji +": locked: "+srsitem.locked+", manualLock: "+ srsitem.manualLock); return srsitem; } function isKanjiLocked(srsitem, kanjiList){ if (!locksOn){ return "no"; } //scriptLog("isKanjiLocked(srsitem, kanjiList)"); //functions: // getCompKanji(srsitem.kanji, kanjiList) //item unlocked by default //may have no kanji, only unlocked kanji will get through the code unflagged var locked = "no"; scriptLog("initialise 'locked': "+ locked); //get the kanji characters in the word. var componentList = getCompKanji(srsitem.kanji, kanjiList); // eg: componentList = getCompKanji("折り紙", kanjiList); // componentList = [{"kanji": "折", "srs": "guru"}, {"kanji": "紙", "srs": "apprentice"}] //scriptLog("isKanjiLocked -> kanjiList = "+JSON.stringify(kanjiList)); scriptLog("components: "+JSON.stringify(componentList)); var c = componentList.length; while(c--){ //look for locked kanji in list if (componentList[c].srs == "apprentice" || componentList[c].srs == "noServerResp"|| componentList[c].srs == "unreached" ){ //----could be apprentice etc. //Simple: lock is 'yes' locked = "yes"; // "yes": item will be locked while there is no database connection. // if the server response indicates that it has been unlocked, only then will it be available for review scriptLog("test srs for apprentice etc. 'locked': "+ locked); scriptLog(componentList[c].kanji +": "+componentList[c].srs +" -> "+ locked); break; // as soon as one kanji is locked, the whole item is locked } //DB locks get special state if (componentList[c].srs == "noMatchWK" || componentList[c].srs == "noMatchGuppy"){ locked = "DB"; //"DB" : database limitations, one of two things //a. the kanji isn't in the database and the user is a guppy --could change if user subscribes or first two levels change/expand //b. the kanji isn't in the database and the user is a turtle --could change if more kanji added. scriptLog("test srs for unmatched kanji. 'locked': "+ locked); scriptLog(componentList[c].kanji +": "+componentList[c].srs +" -> "+ locked); // scriptLog(kanjiList); } } //for char in componentList scriptLog("out of character loop"); //locked will be either "yes","no", or "DB" return locked; } //-------- /* * Edit Items */ window.WKSS_edit = function () { generateEditOptions(); $("#edit").show(); //hide other windows $("#export").hide(); $("#import").hide(); $("#add").hide(); $("#selfstudy").hide(); }; $("body").append(" \ <div id=\"edit\" class=\"WKSS\"> \ <form id=\"editForm\"> \ <button id=\"EditCloseBtn\" class=\"wkss-close\" type=\"button\"><i class=\"icon-remove\"></i></button>\ <h1>Edit your Vocab</h1> \ <select id=\"editWindow\" size=\"8\"></select>\ <input type=\"text\" id=\"editItem\" name=\"\" size=\"40\" placeholder=\"Select vocab, click edit, change and save!\">\ \ <p id=\"editStatus\">Ready to edit..</p>\ <button id=\"EditEditBtn\" type=\"button\">Edit</button>\ <button id=\"EditSaveBtn\" type=\"button\">Save</button> \ <button id=\"EditDeleteBtn\" type=\"button\" title=\"Delete selected item\">Delete</button> \ <button id=\"EditDeleteAllBtn\" type=\"button\" title=\"本当にやるの?\">Delete All</button> \ <button id=\"ResetLevelsBtn\" type=\"button\">Reset levels</button> \ </form> \ </div>"); $("#edit").hide(); $("#ResetLevelsBtn").click(function () { var srslist = getSrsList(); if (srslist) { var i = srslist.length; while(i--){ srslist[i].level = 0; setSrsItem(srslist[i]); } } }); $("#EditEditBtn").click(function () { //get handle for 'select' area var select = document.getElementById("editWindow"); //get the index for the currently selected item var index = select.selectedIndex; //select.options[select.selectedIndex].value is not required, option values are set to index var vocabList = getVocList(); vocabList = vocabList.reverse(); document.getElementById("editItem").value = JSON.stringify(vocabList[index]); document.getElementById("editItem").name = index; //using name to save the index $("#editStatus").text('Loaded item to edit'); }); $("#EditSaveBtn").click(function () { if ($("#editItem").val().length !== 0) { //want to set i when pulling out, and delete when putting in, //deleting one item may cause mismatch if i is property of item in list try { var index = document.getElementById("editItem").name; var item = JSON.parse(document.getElementById("editItem").value.toLowerCase()); var m = item.meaning.length; while(m--){ if (item.meaning[m] === ""){ delete item.meaning[m]; } } var fullList = getFullList().reverse(); if (isItemValid(item) &&//item is valid !(checkForDuplicates(fullList,item) && //kanji (if changed) is not already in the list fullList[index].kanji !== item.kanji)) {//unless it is the item being edited var srslist = getSrsList(); //get srs components of item(list) fullList[index] = item;//does not have srs stuff, re-add it now fullList[index].date = srslist[index].date; fullList[index].level = srslist[index].level; fullList[index].locked = srslist[index].locked; fullList[index].manualLock = srslist[index].manualLock; localStorage.setItem('User-Vocab', JSON.stringify(fullList)); generateEditOptions(); $("#editStatus").html('Saved changes!'); document.getElementById("editItem").value = ""; document.getElementById("editItem").name = ""; }else{ $("#editStatus").text('Invalid item or duplicate!'); alert(isItemValid(item).toString() +" && !("+ checkForDuplicates(fullList,item).toString()+" && !("+fullList[index].kanji+" !== "+item.kanji+")"); } } catch (e) { $("#editStatus").text(e); } } }); $("#EditDeleteBtn").click(function () { //select options element window var select = document.getElementById("editWindow"); //index of selected item var item = select.options[select.selectedIndex].value; //fetch JSON strings from storage and convert them into Javascript literals var vocabList = getFullList(); //starting at selected index, remove 1 entry (the selected index). if (item > -1) { if (vocabList !== null){ vocabList.splice(item, 1); } } //yuck if (vocabList.length !== 0) { localStorage.setItem('User-Vocab', JSON.stringify(vocabList)); } else { localStorage.removeItem('User-Vocab'); } updateEditGUI(); $("#editStatus").text('Item deleted!'); }); function updateEditGUI(){ generateEditOptions(); document.getElementById("editItem").value = ""; document.getElementById("editItem").name = ""; } $("#EditDeleteAllBtn").click(function () { var deleteAll = confirm("Are you sure you want to delete all entries?"); if (deleteAll) { //drop local storage localStorage.removeItem('User-Vocab'); updateEditGUI(); $("#editStatus").text('All items deleted!'); } }); $("#EditCloseBtn").click(function () { $("#edit").hide(); $("#editForm")[0].reset(); $("#editStatus").text('Ready to edit..'); }); //retrieve values from storage to populate 'editItems' menu function generateEditOptions() { var select = document.getElementById('editWindow'); //clear the menu (blank slate) while (select.firstChild) { select.removeChild(select.firstChild); } //check for items to add if (localStorage.getItem('User-Vocab')) { //retrieve from local storage var vocabList = getVocList(); var srslist = getSrsList(); //build option string var i = vocabList.length; while (i--){ //form element to save string var opt = document.createElement('option'); //dynamic components of string //how long since the item was created var dif = Date.now() - srslist[i].date; //how much time required for this level var hour = srsintervals[srslist[i].level]; var review = ""; //no future reviews if burned if(srslist[i].level == 8) { review = "Never"; } //calculate next relative review time //more time has elapsed than required for the level else if(hour <= dif) { review = "Now" ; } else {//turn number (milliseconds) into relatable string (hours, days, etc) review = ms2str(hour-dif); }//end if review is not 'never' or 'now' var text = vocabList[i].kanji + " & " + vocabList[i].reading + " & " + vocabList[i].meaning + " (" + srslevels[srslist[i].level] + " - Review: " + review + ") Locked: " + srslist[i].manualLock; opt.value = i; opt.innerHTML = text; select.appendChild(opt);//export item to option menu } } } function ms2str(milliseconds){ var num; //number of months weeks hours etc //more time has elapsed than required for the level if(milliseconds <= 0) { return "Now" ; } if(milliseconds > 2628000000) {//About a month num = Math.floor(milliseconds/2628000000).toString()+" month"; if (num !== "1 month"){ return num+"s"; }else{ return num; } } if(milliseconds > 604800000) {//A week num = Math.floor(milliseconds/604800000).toString()+" week"; if (num !== "1 week"){ return num+"s"; }else{ return num; } } if(milliseconds > 86400000) {//A day num = Math.floor(milliseconds/604800000).toString()+" day"; if (num !== "1 day"){ return num+"s"; }else{ return num; } } if(milliseconds > 3600000) {//An hour num = Math.floor(milliseconds/3600000).toString()+" hour"; if (num !== "1 hour"){ return num+"s"; }else{ return num; } } if(milliseconds > 60000) {//A minute num = Math.floor(milliseconds/60000).toString()+" minute"; if (num !== "1 minute"){ return num+"s"; }else{ return num; } } if(milliseconds > 1000) {//A second num = Math.floor(milliseconds/1000).toString()+" second"; if (num !== "1 second"){ return num+"s"; }else{ return num; } } } /* * Export */ window.WKSS_export = function () { $("#export").show(); //hide other windows $("#add").hide(); $("#import").hide(); $("#edit").hide(); $("#selfstudy").hide(); }; $("body").append(' \ <div id="export" class="WKSS"> \ <form id="exportForm"> \ <button id="ExportCloseBtn" class="wkss-close" type="button"><i class="icon-remove"></i></button>\ <h1>Export Items</h1> \ <textarea cols="50" rows="18" id="exportArea" placeholder="Export your stuff! Sharing is caring ;)"></textarea> \ \ <p id="exportStatus">Ready to export..</p> \ <button id="ExportItemsBtn" type="button">Export Items</button>\ <button id="ExportSelectAllBtn" type="button">Select All</button>\ </form> \ </div>'); $("#export").hide(); $("#ExportItemsBtn").click(function () { if (localStorage.getItem('User-Vocab')) { $("#exportForm")[0].reset(); var vocabList = getVocList(); $("#exportArea").text(JSON.stringify(vocabList)); $("#exportStatus").text("Copy this text and share it with others!"); } else { $("#exportStatus").text("Nothing to export yet :("); } }); $("#ExportSelectAllBtn").click(function () { if ($("#exportArea").val().length !== 0) { select_all("exportArea"); $("#exportStatus").text("Don't forget to CTRL + C!"); } }); $("#ExportCloseBtn").click(function () { $("#export").hide(); $("#exportForm")[0].reset(); $("#exportArea").text(""); $("#exportStatus").text('Ready to export..'); }); /* * Import */ window.WKSS_import = function () { $("#import").show(); //hide other windows $("#add").hide(); $("#export").hide(); $("#edit").hide(); $("#selfstudy").hide(); }; $("body").append(' \ <div id="import" class="WKSS"> \ <form id="importForm"> \ <button id="ImportCloseBtn" class="wkss-close" type="reset"><i class="icon-remove"></i></button>\ <h1>Import Items</h1>\ <textarea cols="50" rows="18" id="importArea" placeholder="Paste your stuff and hit the import button! Use with caution!"></textarea> \ \ <p id="importStatus">Ready to import..</p> \ <label class="button" id="ImportItemsBtn" style="display:inline;">Import Items</label>\ \ <label id="ImportCsvBtn" class="button" style="display:inline;cursor: pointer;">Import CSV \ \ <input type="file" id="upload" accept=".csv,.tsv" style="height:0px;width:0px;background:red;opacity:0;filter:opacity(1);" />\ \ </label>\ \ <label class="button" id="ImportWKBtn" style="display:inline;"><i class="icon-download-alt"></i> WK</label>\ </form> \ </div>'); $("#import").hide(); function fileUpload (ev){ var csvHeader = true; //first row contains stuff like "Kanji/Vocab, Reading, Meaning" etc var tsvfile; //tabs separate fields, commas seperate values? or false for vice versa var CSVs = ev.target.files; var name =CSVs[0].name; var colsplit, vsplit; if (name.substr(name.lastIndexOf("."),4)===".csv"){ tsvfile = false; colsplit = ","; vsplit = "\t"; }else{ tsvfile = true; colsplit = "\t"; vsplit = ","; } scriptLog("tsvfile: "+tsvfile.toString()); scriptLog("file uploaded: "+CSVs[0].name); var reader = new FileReader(); reader.readAsText(CSVs[0]); reader.onload = function(ev){ var csvString = ev.target.result; var csvRow = csvString.split("\n"); //default column rows var k = 0; var r = 1; var m = 2; var i = csvRow.length; //process header, changing k,r,m if necessary var JSONimport = []; while(i--){ var row = csvRow[i]; if ((csvHeader === true && i === 0)|| // Skip header (row === "") // Skip empty rows ){ scriptLog("Skipping row #"+i); }else{ scriptLog(row); var elem = row.split(colsplit); var item = {}; var c; if (elem[k]){ item.kanji = elem[k].trim(); if (elem[r]){ if (elem[r].indexOf(vsplit)>-1){ // eg 'reading 1[tab]reading 2[tab]reading 3' item.reading = elem[r].split(vsplit); }else{ //no tabs in string, single value item.reading=[elem[r]]; } }else{ item.reading=[""]; } if (elem[m]){ if (elem[m].indexOf(vsplit)>-1){ // eg 'meaning 1[tab]meaning 2[tab]meaning 3' item.meaning = elem[m].split("\t"); }else{ //no tabs in string, single value item.meaning=[elem[m]]; } c = item.meaning.length; while(c--){ scriptLog("item.meaning["+c+"]: "+item.meaning[c]); } }else{//todo: provide overwrite option on forced meaning item.meaning=[""]; } JSONimport.push(item); }else{ // corrupt row ('kanji' is mandatory (can be kana-only word), is not present on row, skip } } } var JSONstring = JSON.stringify(JSONimport); scriptLog(JSONstring); if (JSONstring.length !== 0) { try { var add = JSON.parse(JSONstring.toLowerCase()); /*//---------/------------- if (!checkAdd(add)) { $("#importStatus").text("No valid input (duplicates?)!"); return; } //----------------------*/ var a = add.length; while(a--){ setVocItem(add[a]); } $("#importStatus").text("Import successful!"); $("#importForm")[0].reset(); $("#importArea").text(""); } catch (e) { $("#importStatus").text("Parsing Error!"); scriptLog(e); } } else { $("#importStatus").text("Nothing to import :( Please paste your stuff first"); } }; } document.getElementById("upload").addEventListener('change', fileUpload, false); $("#ImportCsvBtn").click(function () { }); $("#ImportWKBtn").click(function(){ getServerResp(APIkey,"vocabulary"); scriptLog("maybe?"); }); $("#ImportItemsBtn").click(function () { if ($("#importArea").val().length !== 0) { try { var add = JSON.parse($("#importArea").val().toLowerCase()); alert(JSON.stringify(add)); if (checkAdd(add)) { $("#importStatus").text("No valid input (duplicates?)!"); return; } var newlist; var srslist = []; if (localStorage.getItem('User-Vocab')) { var vocabList = getVocList(); srslist = getSrsList(); newlist = vocabList.concat(add); } else { newlist = add; } var i = add.length; while(i--){ setVocItem(add[i]); } $("#importStatus").text("Import successful!"); $("#importForm")[0].reset(); $("#importArea").text(""); } catch (e) { $("#importStatus").text("Parsing Error!"); scriptLog(e); } } else { $("#importStatus").text("Nothing to import :( Please paste your stuff first"); } }); $("#ImportCloseBtn").click(function () { $("#import").hide(); $("#importForm")[0].reset(); $("#importArea").text(""); $("#importStatus").text('Ready to import..'); }); /* * Review Items */ window.WKSS_review = function () { //is there a session waiting in storage? if(sessionStorage.getItem('User-Review')) { scriptLog("There is a session ready "+JSON.stringify(sessionStorage.getItem('User-Review'))); //show the selfstudy window $("#selfstudy").show(); //hide other windows $("#add").hide(); $("#export").hide(); $("#edit").hide(); $("#import").hide(); startReview(); } }; $("body").append(' \ <div id="selfstudy" class="WKSS">\ <button id="SelfstudyCloseBtn" class="wkss-close" type="button"><i class="icon-remove"></i></button>\ <h1>Review<span id="RevNum"></span></h1>\ <div id="wkss-kanji">\ <span id="rev-kanji"></span>\ </div><div id="wkss-type">\ <span id="rev-type"></span><br />\ </div><div id="wkss-solution">\ <span id="rev-solution"></span>\ </div><div id="wkss-input">\ <input type="text" id="rev-input" size="40" placeholder="">\ </div><span id="rev-index" style="display: block;"></span>\ \ <form id="audio-form">\ <label id="AudioButton" class="button">Play audio</label>\ <label id="WrapUpBtn" class="button">Wrap Up</label>\ </form>\ <div id="rev-audio" style="display:none;"></div>\ </div>'); $("#selfstudy").hide(); $("#SelfstudyCloseBtn").click(function () { $("#selfstudy").hide(); $("#rev-input").val(""); reviewActive = false; }); $("#WrapUpBtn").click(function() { var sessionList = JSON.parse(sessionStorage.getItem('User-Review')); var statsList = JSON.parse(sessionStorage.getItem('User-Stats')); //if an index in sessionList matches one in statsList, don't delete var sessionI = sessionList.length; var arr2 = []; //for every item in sessionList, look for index in statsList, //if not there (-1) delete item from sessionList while (sessionI--){ var index = findIndex(statsList,sessionList[sessionI]); if (Math.sign(1/index) !== -1){ arr2.push(sessionList[sessionI]); }else// if (index -1){ //}else { scriptLog("delete " + index); // break; //shouldn't get here, but don't want infinite loop if it does } } scriptLog(arr2); sessionStorage.setItem('User-Review', JSON.stringify(arr2)); }); //--------- // save to list based on .index property function saveToSortedList(eList,eItem){ var get = findIndex(eList,eItem); if (Math.sign(1/get) === -1){ eList.splice(-get,0,eItem); return eList; } } function findIndex(values, target) { return binarySearch(values, target, 0, values.length - 1); } function binarySearch(values, target, start, end) { scriptLog("binarySearch(values: ,target: , start: "+start+", end: "+end+")"); if (start > end) { //start has higher value than target, end has lower value //item belongs between // need to return 'start' with a flag that it hasn't been found //invert sign :) return -(start); //for testing truths // return String(end)+" < "+item.index+" < "+String(start); } //does not exist var middle = Math.floor((start + end) / 2); var value = values[middle]; scriptLog("start.index: "+values[start].index); scriptLog("middle.index: "+values[middle].index); scriptLog("end.index: "+values[end].index); if (Number(value.index) > Number(target.index)) { return binarySearch(values, target, start, middle-1); } if (Number(value.index) < Number(target.index)) { return binarySearch(values, target, middle+1, end); } return middle; //found! } //------- $("#AudioButton").click(function () { OpenInNewTab(document.getElementById('rev-audio').innerHTML); }); function OpenInNewTab(url ) { var win=window.open(url, '_blank'); win.focus(); } function playAudio() { var kanji = document.getElementById('rev-kanji').innerHTML; var kana = (document.getElementById('rev-solution').innerHTML.split(/[,、]+\s*/))[0]; document.getElementById('rev-audio').innerHTML = ""; document.getElementById('audio-form').action = ""; //document.getElementById('AudioButton').disabled = true; if( !kanji.match(/[a-zA-Z]+/i) && !kana.match(/[a-zA-Z]+/i)) { kanji = encodeURIComponent(kanji); kana = encodeURIComponent(kana); var i; var newkanji = ""; for(i = 1; i < kanji.length; i = i+3) { newkanji = newkanji.concat(kanji[i-1]); newkanji = newkanji.concat('2'); newkanji = newkanji.concat('5'); newkanji = newkanji.concat(kanji[i]); newkanji = newkanji.concat(kanji[i+1]); } var newkana = ""; for(i = 1; i < kana.length; i = i+3) { newkana = newkana.concat(kana[i-1]); newkana = newkana.concat('2'); newkana = newkana.concat('5'); newkana = newkana.concat(kana[i]); newkana = newkana.concat(kana[i+1]); } var url = "http://www.csse.monash.edu.au/~jwb/audiock.swf?u=kana=" + newkana + "%26kanji=" + newkanji; scriptLog("Audio URL: " + url); document.getElementById('AudioButton').disabled = false; document.getElementById('rev-audio').innerHTML = url; } } function generateReviewList() { //don't interfere with an active session if (reviewActive){ document.getElementById('user-review').innerHTML = "Review in Progress"; return; } scriptLog("generateReviewList()"); // function generateReviewList() builds a review session and updates the html menu to show number waiting. var numReviews = 0; var soonest; var next; var reviewList = []; //check to see if there is vocab already in offline storage if (localStorage.getItem('User-Vocab')) { var vocabList = getFullList(); scriptLog("var vocabList = localStorage.getItem('User-Vocab') (" + JSON.stringify(vocabList)+")"); var srsList = getSrsList(); scriptLog("var srsList = localStorage.getItem('User-Vocab') (" + JSON.stringify(srsList)+")"); var now = Date.now(); //for each vocab in storage, get the amount of time vocab has lived var i = vocabList.length; while(i--){ var dif = now - vocabList[i].date; // if tem is unlocked and unburned if (vocabList[i].level < 8 && (vocabList[i].manualLock === "no" || vocabList[i].manualLock ==="DB" && !lockDB )){ // if it is past review time if(srsintervals[vocabList[i].level] <= dif) { // count vocab up for review numReviews++; // add item-meaning object to reviewList // have made this optional for surname lists etc. if (vocabList[i].meaning[0] !== "") { var revItem = {}; revItem.prompt = vocabList[i].kanji; revItem.kanji = vocabList[i].kanji; revItem.type = "Meaning"; revItem.solution = vocabList[i].meaning; revItem.index = i; reviewList.push(revItem); } // reading is optional, if there is a reading for the vocab, add its object. if (vocabList[i].reading[0] !== "") { var revItem2 = {}; revItem2.prompt = vocabList[i].kanji; revItem2.kanji = vocabList[i].kanji; revItem2.type = "Reading"; revItem2.solution = vocabList[i].reading; // item and item2 are matched by mutual index revItem2.index = i; reviewList.push(revItem2); } //if there is a meaning and reading, and reverse flag is true, test reading from english if (vocabList[i].reading[0] !== "" && vocabList[i].meaning[0] !== "" && reverse){ var revItem3 = {}; revItem3.prompt = vocabList[i].meaning.join(", "); revItem3.kanji = vocabList[i].kanji; revItem3.type = "Reverse"; revItem3.solution = vocabList[i].reading; // item and item2 are matched by mutual index revItem3.index = i; reviewList.push(revItem3); } }else{//unlocked/unburned but not time to review yet scriptLog("setting soonest"); next = srsintervals[vocabList[i].level] - dif; if(soonest){ soonest = Math.min(soonest, next); }else{ soonest = next; } } }//end if item is up for review }// end iterate through vocablist }// end if localStorage if (reviewList.length !== 0){ //shuffle the remainder of the deck reviewList = shuffle(reviewList); //store reviewList in current session sessionStorage.setItem('User-Review', JSON.stringify(reviewList)); scriptLog("sessionStorage.setItem('User-Review, "+JSON.stringify(reviewList)+")"); }else{ scriptLog("reviewList is empty: "+JSON.stringify(reviewList)); document.getElementById('user-review').innerHTML = "Next Review in "+ms2str(soonest); } var strReviews = numReviews.toString(); /* If you want to do the 42+ thing. if (numReviews > 42) { strReviews = "42+"; //hail the crabigator! } //*/ // return the number of reviews scriptLog(numReviews.toString() +" reviews created"); if (numReviews > 0){ document.getElementById('user-review').innerHTML = "Review (" + strReviews + ")"; } } //global to keep track of when a review is in session. var reviewActive = false; function startReview() { scriptLog("startReview()"); reviewActive = true; //get the review 'list' from session storage, line up the first item in queue var reviewList = JSON.parse(sessionStorage.getItem('User-Review')); nextReview(reviewList); } function nextReview(reviewList) { //sets up the next item for review //uses functions: // wanakana.bind/unbind var item = reviewList[0]; if (sessionStorage.getItem('User-Stats')){ $("#RevNum").innerHtml = JSON.parse(sessionStorage.getItem('User-Stats')).length; scriptLog(JSON.parse(sessionStorage.getItem('User-Review')).length+" reviews left"); } document.getElementById('rev-kanji').innerHTML = item.prompt; document.getElementById('rev-type').innerHTML = item.type; document.getElementById('rev-solution').innerHTML = item.solution; document.getElementById('rev-index').innerHTML = item.index; //initialise the input field $("#rev-input").focus(); $("#rev-input").removeClass("error"); $("#rev-input").removeClass("correct"); $("#rev-input").val(""); //check for alphabet letters and decide to bind or unbind wanakana if (item.solution[0].match(/[a-zA-Z]+/i)) { wanakana.unbind(document.getElementById('rev-input')); $('#rev-input').attr('placeholder','Your response'); //I don't know why this was put here, it overwrites the 'type' that we already know it should be. //document.getElementById('rev-type').innerHTML = "Meaning"; } else { wanakana.bind(document.getElementById('rev-input')); $('#rev-input').attr('placeholder','答え'); //document.getElementById('rev-type').innerHTML = "Reading"; } playAudio(); } function storeSession(correct,item){ //shim to facilitate renaming function markAnswer(item); } function markAnswer(item) { //evaluate 'item' against the question. // match by index // get type of question // determine if right or wrong and store result appropriately //get the question var prompt = document.getElementById('rev-kanji').innerHTML.trim(); //get the answer var answer = $("#rev-input").val().toLowerCase(); //get the index var index = document.getElementById('rev-index').innerHTML.trim(); //get the question type var type = document.getElementById('rev-type').innerHTML.trim(); //get the item if it is in the current session var storedItem = JSON.parse(sessionStorage.getItem(item.index)); if (storedItem){ item.numCorrect = storedItem.numCorrect; item.numWrong = storedItem.numWrong; } if (index == item.index||true){ if (inputCorrect()){ scriptLog(answer+"/"+item.solution[0]); if (!item.numCorrect){ scriptLog("initialising numCorrect"); item.numCorrect={}; } scriptLog("Correct: "+ type); if (type == "Meaning"){ if (!item.numCorrect.Meaning) item.numCorrect.Meaning = 0; item.numCorrect.Meaning++; } if (type == "Reading"){ if (!item.numCorrect.Reading) item.numCorrect.Reading = 0; item.numCorrect.Reading++; } if (type == "Reverse"){ if (!item.numCorrect.Reverse) item.numCorrect.Reverse = 0; item.numCorrect.Reverse++; // */ } }else{ scriptLog(answer+"!="+item.solution); if (!item.numWrong){ scriptLog("initialising numCorrect"); item.numWrong={}; } scriptLog("Wrong: "+ type); if (type == "Meaning"){ if (!item.numWrong.Meaning) item.numWrong.Meaning = 0; item.numWrong.Meaning++; } if (type == "Reading"){ if (!item.numWrong.Reading) item.numWrong.Reading = 0; item.numWrong.Reading++; } if (type == "Reverse"){ if (!item.numWrong.Reverse) item.numWrong.Reverse = 0; item.numWrong.Reverse++; } } scriptLog("right: "+JSON.stringify(item.numCorrect)+", wrong: "+JSON.stringify(item.numWrong)); } else { scriptLog("indexes don't match"); } sessionStorage.setItem(item.index, JSON.stringify(item)); //for one item only for now var list = JSON.parse(sessionStorage.getItem("User-Stats")); var found = false; if (list){ var i = list.length; while(i--){ if (list[i].index == item.index) { list[i] = item; //replace item if it exists found = true; break; } } if(!found){ scriptLog("Attempt sorted save"); list = saveToSortedList(list,item); // list.push(item); //push item if it doesn't } } else { list = [item]; } sessionStorage.setItem("User-Stats", JSON.stringify(list)); scriptLog("-------------------------------------------"); } function showResults() { if (sessionStorage.getItem('User-Stats')) { var statsList = JSON.parse(sessionStorage.getItem('User-Stats')); var i = statsList.length; while(i--){ var slist = getSrsList(); //slist[statsList[i].index].level; var altText = slist[statsList[i].index].level;//+statsList[i].type; if (!statsList[i].numWrong) { if (statsList[i].numCorrect){ if (statsList[i].numCorrect.Meaning) altText = altText + " Meaning Correct x"+statsList[i].numCorrect.Meaning +"\n"; if (statsList[i].numCorrect.Reading) altText = altText + " Reading Correct x"+statsList[i].numCorrect.Reading +"\n"; if (statsList[i].numCorrect.Reverse) altText = altText + " Reverse Correct x"+statsList[i].numCorrect.Reverse +"\n"; } document.getElementById("stats-a").innerHTML += "<span class=\"rev-correct\" title='"+altText+" +'>" + statsList[i].kanji + "</span>"; } else { if (statsList[i].numWrong.Meaning) altText = altText + " Meaning Wrong x"+statsList[i].numWrong.Meaning +"\n"; if (statsList[i].numWrong.Reading) altText = altText + " Reading Wrong x"+statsList[i].numWrong.Reading +"\n"; if (statsList[i].numWrong.Reverse) altText = altText + " Reverse Wrong x"+statsList[i].numWrong.Reverse +"\n"; if (statsList[i].numCorrect){ if (statsList[i].numCorrect.Meaning) altText = altText + " Meaning Correct x"+statsList[i].numCorrect.Meaning +"\n"; if (statsList[i].numCorrect.Reading) altText = altText + " Reading Correct x"+statsList[i].numCorrect.Reading +"\n"; if (statsList[i].numCorrect.Reverse) altText = altText + " Reverse Correct x"+statsList[i].numCorrect.Reverse +"\n"; } document.getElementById("stats-a").innerHTML += "<span class=\"rev-error\" title='"+altText+"'>" + statsList[i].kanji + "</span>"; } statsList[i] = updateSRS(statsList[i]); } scriptLog(statsList); } //*/ //clear session sessionStorage.clear(); reviewActive = false; } $("body").append(' \ <div id="resultwindow" class="WKSS"> \ <button id="ReviewresultsCloseBtn" class="wkss-close" type="button"><i class="icon-remove"></i></button>\ <h1>Review Results</h1>\ <h2>All</h2>\ <div id="stats-a"></div>\ </div>'); $("#resultwindow").hide(); $("#ReviewresultsCloseBtn").click(function () { $("#resultwindow").hide(); document.getElementById("stats-a").innerHTML = ""; }); //declare global values for keyup event //is an answer being submitted? var submit = true; //jquery keyup event $("#rev-input").keyup(function (e) { //functions: // inputCorrect() //check if key press was 'enter' (keyCode 13) on the way up //and keystate true (answer being submitted) //and cursor is focused in reviewfield if (e.keyCode == 13 && submit === true) { var sessionList = JSON.parse(sessionStorage.getItem('User-Review')); var item = sessionList[0]; if (item === null){ sessionList.splice(0, 1); }else{ //handle grading and storing solution //check for input, do nothing if none if($("#rev-input").val().length === 0){ return; } //disable input after submission //document.getElementById('rev-input').disabled = true; //was the input correct? var correct = inputCorrect(); if (correct) { //highlight in (default) green $("#rev-input").addClass("correct"); } else { //highight in red $("#rev-input").addClass("error"); } //show answer $("#rev-solution").addClass("info"); //remove from sessionList if correct if (correct) { scriptLog("correct answer"); if (sessionList !== null){ var oldlen = sessionList.length; sessionList.splice(0, 1); scriptLog("sessionList.length: "+ oldlen +" -> "+sessionList.length); //replace shorter (by one) sessionList to session if (sessionList.length !== 0) { scriptLog("sessionList.length: "+ sessionList.length); sessionStorage.setItem('User-Review', JSON.stringify(sessionList)); } else { //reveiw over, delete sessionlist from session scriptLog("sessionStorage.removeItem('User-Review')"); sessionStorage.removeItem('User-Review'); } }else{ scriptLog("Error: no session found"); } }else{ scriptLog("wrong answer"); } scriptLog("store session. item: "+JSON.stringify(item)); storeSession(correct,item); //playAudio(); scriptLog("store session complete"); //answer submitted, next 'enter' proceeds with script submit = false; }//null garbage collection } else if (e.keyCode == 13 && submit === false) { scriptLog("keystat = " + submit); //there are still more reviews in session? if (sessionStorage.getItem('User-Review')) { scriptLog("found a 'User-Review': " + sessionStorage.getItem('User-Review')); setTimeout(function () { scriptLog("refreshing reviewList from storage"); var reviewList = JSON.parse(sessionStorage.getItem('User-Review')); //cue up first remaining review nextReview(reviewList); scriptLog("checking for empty reviewList"); if (reviewList.length !== 0){ // sessionStorage.setItem('User-Review', JSON.stringify(reviewList)); }else{ scriptLog("session over. reviewList: "+JSON.stringify(reviewList)); sessionStorage.removeItem("User-Review"); } // document.getElementById('rev-input').disabled = true; $("#rev-solution").removeClass("info"); $("#selfstudy").hide().fadeIn('fast'); }, 1); } else { // no review stored in session, review is over setTimeout(function () { $("#selfstudy").hide(); //document.getElementById('rev-input').disabled = false; $("#rev-solution").removeClass("info"); scriptLog("showResults"); showResults(); $("#resultwindow").show(); scriptLog("showResults completed"); }, 1); } submit = true; scriptLog("submit = " + submit); } }); function updateSRS(stats) { var srslist = getSrsList(); // var now = Date.now(); if(!stats.numWrong) {//all correct (none wrong) //alert("pre level: srslist[stats.index].i/stats.index "+srslist[stats.index].i+"/"+stats.index); srslist[stats.index].level++; // alert("level up: "+srslist[stats.index].level); } else { if(srslist[stats.index].level > 0) srslist[stats.index].level--; //alert("level down: "+srslist[stats.index].level); } srslist[stats.index].date = now; // scriptLog("updateSRS - " + srslist[index].kanji + " - new level: " + srslist[index].level + " date: " + now); //FIX need to avoid getting and setting for every item. localStorage.setItem("User-Vocab", JSON.stringify(srslist)); } function inputCorrect() { var input = $("#rev-input").val().toLowerCase(); var solution = document.getElementById('rev-solution').innerHTML.split(/[,、]+\s*/); var correctCharCount = 0; var returnvalue = false; scriptLog("Input: " + input); // also allow entering of both solutions at once, if available if(solution.length == 2) { solution[2] = solution[0] + ", " + solution[1]; solution[3] = solution[1] + ", " + solution[0]; } var i = solution.length; while(i--){ solution[i] = solution[i].toLowerCase(); var threshold = 0;//how many characters can be wrong if(document.getElementById('rev-type').innerHTML == "Meaning") { threshold = Math.floor(solution[i].length / errorAllowance); } scriptLog("Checking " + solution[i] + " with threshold: " + threshold); var j; var lengthDiff = Math.abs(input.length - solution[i].length); if (lengthDiff > threshold){ returnvalue = returnvalue || false; scriptLog("false at Eth if branch " + input.length + " < " + (solution[i].length - threshold)); } else { //difference in response length is within threshold j = input.length; while (j--) { if (input[j] == solution[i][j]) { scriptLog (input[j] +" == "+ solution[i][j]); correctCharCount++; } } if ((solution[i].length - threshold) <= correctCharCount) { returnvalue = returnvalue || true; scriptLog("true at Eth if branch " + (solution[i].length - threshold) + " <= " + correctCharCount); } } } scriptLog("Returning " + returnvalue); return returnvalue; } /* * Adds the Button */ function addUserVocabButton() { scriptLog("addUserVocabButton()"); //Functions (indirect) // WKSS_add() // WKSS_edit() // WKSS_export() // WKSS_import() // WKSS_lock() // WKSS_review() var nav = document.getElementsByClassName('nav'); scriptLog("generating review list because: initialising script and populating reviews"); if (nav) { nav[2].innerHTML = nav[2].innerHTML + "\n\ <li class=\"dropdown custom\">\n\ <a class=\"dropdown-toggle custom\" data-toggle=\"dropdown\" href=\"#\" onclick=\"generateReviewList();\">\n\ <span lang=\"ja\">自習</span>\n\ Self-Study <i class=\"icon-chevron-down\"></i>\n\ </a>\n\ <ul class=\"dropdown-menu\" id=\"WKSS_dropdown\">\n\ <li class=\"nav-header\">Customize</li>\n\ <li><a id=\"click\" href=\"#\" onclick=\"WKSS_add();\">Add</a></li>\n\ <li><a href=\"#\" onclick=\"WKSS_edit();\">Edit</a></li>\n\ <li><a href=\"#\" onclick=\"WKSS_export();\">Export</a></li>\n\ <li><a href=\"#\" onclick=\"WKSS_import();\">Import</a></li>\n\ <!--// <li><a href=\"#\" onclick=\"WKSS_lock();\">Server Settings</a></li>//-->\n\ <li class=\"nav-header\">Learn</li>\n\ <li><a id=\"user-review\" href=\"#\" onclick=\"WKSS_review();\">Please wait...</a></li>\n\ </ul>\n\ </li>"; } } /* * Prepares the script */ function scriptInit() { scriptLog("scriptInit()"); //functions: // addUserVocabButton() // logError(err) scriptLog("Initializing Wanikani UserVocab Script!"); GM_addStyle(".custom .dropdown-menu {background-color: #DBA901 !important;}"); GM_addStyle(".custom .dropdown-menu:after {border-bottom-color: #DBA901 !important;"); GM_addStyle(".custom .dropdown-menu:before {border-bottom-color: #DBA901 !important;"); GM_addStyle(".open .dropdown-toggle.custom {background-color: #FFC400 !important;}"); GM_addStyle(".custom .dropdown-menu a:hover {background-color: #A67F00 !important;}"); GM_addStyle(".custom:hover {color: #FFC400 !important;}"); GM_addStyle(".custom:hover span {border-color: #FFC400 !important;}"); GM_addStyle(".custom:focus {color: #FFC400 !important;}"); GM_addStyle(".custom:focus span {border-color: #FFC400 !important;}"); GM_addStyle(".open .custom span {border-color: #FFFFFF !important;}"); GM_addStyle(".open .custom {color: #FFFFFF !important}"); GM_addStyle(" \ .WKSS {\ position:fixed;\ z-index: 2;\ top:125px;\ left:50%;\ margin:0px;\ background: #FFF;\ padding: 5px;\ font: 12px \"ヒラギノ角ゴ Pro W3\", \"Hiragino Kaku Gothic Pro\",Osaka, \"メイリオ\", Meiryo, \"MS Pゴシック\", \"MS PGothic\", sans-serif;\ color: #888;\ text-shadow: 1px 1px 1px #FFF;\ border:1px solid #DDD;\ border-radius: 5px;\ -webkit-border-radius: 5px;\ -moz-border-radius: 5px;\ box-shadow: 10px 10px 5px #888888;\ }\ .WKSS h1 {\ font: 25px \"ヒラギノ角ゴ Pro W3\", \"Hiragino Kaku Gothic Pro\",Osaka, \"メイリオ\", Meiryo, \"MS Pゴシック\", \"MS PGothic\", sans-serif;\ padding-left: 5px;\ display: block;\ border-bottom: 1px solid #DADADA;\ margin: 0px;\ color: #888;\ }\ .WKSS h1>span {\ display: block;\ font-size: 11px;\ }\ .WKSS label {\ display: block;\ margin: 0px 0px 5px;\ }\ \ \ .WKSS label>span {\ float: left;\ width: 80px;\ text-align: right;\ padding-right: 10px;\ margin-top: 10px;\ color: #333;\ font-family: \"ヒラギノ角ゴ Pro W3\", \"Hiragino Kaku Gothic Pro\",Osaka, \"メイリオ\", Meiryo, \"MS Pゴシック\", \"MS PGothic\", sans-serif;\ font-weight: bold;\ }\ .WKSS input[type=\"text\"], .WKSS input[type=\"email\"], .WKSS textarea{\ border: 1px solid #CCC;\ color: #888;\ height: 20px;\ margin-bottom: 16px;\ margin-right: 6px;\ margin-top: 2px;\ outline: 0 none;\ padding: 6px 12px;\ width: 80%;\ border-radius: 4px;\ line-height: normal !important;\ -webkit-border-radius: 4px;\ -moz-border-radius: 4px;\ font: normal 14px/14px \"ヒラギノ角ゴ Pro W3\", \"Hiragino Kaku Gothic Pro\",Osaka, \"メイリオ\", Meiryo, \"MS Pゴシック\", \"MS PGothic\", sans-serif;\ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\ }\ .WKSS select {\ border: 1px solid #CCC;\ color: #888;\ outline: 0 none;\ padding: 6px 12px;\ height: 160px !important;\ width: 95%;\ border-radius: 4px;\ -webkit-border-radius: 4px;\ -moz-border-radius: 4px;\ font: normal 14px/14px \"ヒラギノ角ゴ Pro W3\", \"Hiragino Kaku Gothic Pro\",Osaka, \"メイリオ\", Meiryo, \"MS Pゴシック\", \"MS PGothic\", sans-serif;\ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\ #background: #FFF url('down-arrow.png') no-repeat right;\ #background: #FFF url('down-arrow.png') no-repeat right);\ appearance:none;\ -webkit-appearance:none;\ -moz-appearance: none;\ text-indent: 0.01px;\ text-overflow: '';\ }\ .WKSS textarea{\ height:100px;\ }\ .WKSS button, .button {\ position: relative;\ background: #FFF;\ border: 1px solid #CCC;\ padding: 10px 25px 10px 25px;\ color: #333;\ border-radius: 4px;\ display: inline !important;\ }\ .WKSS button:disabled {\ background: #EBEBEB;\ border: 1px solid #CCC;\ padding: 10px 25px 10px 25px;\ color: #333;\ border-radius: 4px;\ }\ .WKSS .button:hover, button:hover:enabled {\ color: #333;\ background-color: #EBEBEB;\ border-color: #ADADAD;\ } \ .WKSS button:hover:disabled {\ cursor: default\ } \ .error {border-color:#F00 !important; color: #F00 !important;}\ .correct {border-color:#0F0 !important; color: #0F0 !important;}\ .info {border-color:#696969 !important; color: #696969 !important;}\ .rev-error {text-shadow:none; border: 1px solid #F00 !important;border-radius: 10px; background-color: #F00; padding:4px; margin:4px; color: #FFFFFF; font: normal 18px \"ヒラギノ角ゴ Pro W3\", \"Hiragino Kaku Gothic Pro\",Osaka, \"メイリオ\", Meiryo, \"MS Pゴシック\", \"MS PGothic\", sans-serif;}\ .rev-correct {text-shadow:none; border: 1px solid #088A08 !important;border-radius: 10px; background-color: #088A08; padding:4px; margin:4px; color: #FFFFFF; font: normal 18px \"ヒラギノ角ゴ Pro W3\", \"Hiragino Kaku Gothic Pro\",Osaka, \"メイリオ\", Meiryo, \"MS Pゴシック\", \"MS PGothic\", sans-serif;}"); GM_addStyle("\ #add {\ width:" + addWindowWidth + "px;\ height:" + addWindowHeight + "px; \ margin-left:-" + addWindowWidth/2 + "px; \ }"); GM_addStyle("\ #export, #import {\ background:#fff;\ width:" + exportImportWindowWidth + "px;\ height:" + exportImportWindowHeight + "px;\ margin-left:-" + exportImportWindowWidth/2 + "px; \ }"); GM_addStyle("\ #edit {\ width:" + editWindowWidth + "px;\ height:" + editWindowHeight + "px; \ margin-left:-" + editWindowWidth/2 + "px; \ }"); GM_addStyle("\ #selfstudy {\ left:50%;\ width:" + studyWindowWidth + "px;\ height:auto; \ margin-left:-" + studyWindowWidth/2 + "px; \ }"); GM_addStyle("\ #resultwindow {\ left:50%;\ width:" + resultWindowWidth + "px;\ height:" + resultWindowHeight + "px; \ margin-left:-" + resultWindowWidth/2 + "px; \ }"); GM_addStyle("\ #AudioButton {\ margin-top: 35px;\ position: relative;\ display: inline !important;\ -webkit-margin-before: 50px;\ }\ button.wkss-close {\ float:right;\ background-color:#ff4040;\ color:#fff;\ padding:0px;\ height:27px;\ width:27px\ }\ \ #wkss-close {\ float:right;\ background-color:#ff4040;\ color:#fff;\ padding:0px;\ height:27px;\ width:27px\ }\ #wkss-kanji, #rev-kanji {\ text-align:center !important;\ font-size:50px !important;\ background-color: #9400D3 !important;\ color: #FFFFFF !important;\ border-radius: 10px 10px 0px 0px;\ }\ #wkss-solution, #rev-solution {\ text-align: center !important;\ font-size:30px !important;\ color: #FFFFFF;\ padding: 2px;\ }\ #wkss-type, #rev-type {\ text-align:center !important;\ font-size:24px !important;\ background-color: #696969 !important;\ color: #FFFFFF !important;\ border-radius: 0px 0px 10px 10px;\ }\ #wkss-input {\ text-align:center !important;\ font-size:40px !important;\ height: 80px !important;\ line-height: normal !important;\ }\ #rev-input {\ text-align:center !important;\ font-size:40px !important;\ height: 60px !important;\ line-height: normal !important;\ }"); // Set up buttons try { if (typeof(Storage) !== "undefined") { addUserVocabButton(); //provide warning to users trying to use the (incomplete) script. scriptLog("this script is still incomplete: \n\ I recommend you install the original script by shudouken at \n\ http://userscripts-mirror.org/scripts/show/381435"); } else { scriptLog("Wanikani Self-Study: Your browser does not support localStorage.. Sorry :("); } } catch (err) { logError(err); } } /* * Helper Functions/Variables */ function isEmpty(value) { return (typeof value === "undefined" || value === null); } function select_all(str) { //eval can be harmful var text_val = document.getElementById(str); scriptLog(text_val); text_val.focus(); text_val.select(); } function checkAdd(add) { //take a JSON object (parsed from import window) and check with stored items for any duplicates // Returns true if each item in 'add' array is valid and //at least one of them already exists in storage var i = add.length; if(localStorage.getItem('User-Vocab')) { var vocabList = getVocList(); while(i--){ if (isItemValid(add[i]) && checkForDuplicates(vocabList,add[i])) return true; } } return false; } function isItemValid(add) { //validates an object representing vocab return (!isEmpty(add.kanji) && //kanji property exists !isEmpty(add.meaning) && //meaning property exists !isEmpty(add.reading)&& //reading property exists Object.prototype.toString.call(add.meaning) === '[object Array]'&&//meaning is an array Object.prototype.toString.call(add.reading) === '[object Array]');//reading is an array } function shuffle(array) { var currentIndex = array.length; var temporaryValue; var randomIndex; // While there remain elements to shuffle... while (0 !== currentIndex) { // Pick a remaining element... randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // And swap it with the current element. temporaryValue = array[currentIndex]; array[currentIndex] = array[randomIndex]; array[randomIndex] = temporaryValue; } return array; } /* * Error handling * Can use 'error.stack', not cross-browser (though it should work on Firefox and Chrome) */ function logError(error) { scriptLog("logError(error)"); var stackMessage = ""; if ("stack" in error) stackMessage = "\n\tStack: " + error.stack; scriptLog("WKSS: Error: " + error.name + "\n\tMessage: " + error.message + stackMessage); console.error("WKSS: Error: " + error.name + "\n\tMessage: " + error.message + stackMessage); } //*****Ethan's Functions***** function handleReadyStateFour(xhrk, requestedItem){ var localkanjiList = []; scriptLog("readystate: "+ xhrk.readyState); scriptLog("responseText: "+xhrk.responseText); var resp = JSON.parse(xhrk.responseText); scriptLog("about to loop through requested information"); var i=resp.requested_information.length; if (requestedItem === "kanji"){ while(i--){ //push response onto kanjilist variable if (resp.requested_information[i].user_specific !== null){ localkanjiList.push({"character": resp.requested_information[i].character, "srs": resp.requested_information[i].user_specific.srs}); }else{ localkanjiList.push({"character": resp.requested_information[i].character, "srs": "unreached"}); } } }else if(requestedItem === "vocabulary"){ while(i--){ //push response onto kanjilist variable if (resp.requested_information[i].user_specific !== null){ //build vocablist localkanjiList.push({"kanji": resp.requested_information[i].character, "reading": resp.requested_information[i].kana.split(","), "meaning": resp.requested_information[i].meaning.split(",")}); } } } //return kanjiList // scriptLog("Server responded with new kanjiList: \n"+JSON.stringify(kanjiList)); return localkanjiList; } function getServerResp(APIkey, requestedItem){ requestedItem = typeof requestedItem !== 'undefined' ? requestedItem : 'kanji'; //functions: // refreshLocks() // generateReviewList() if (APIkey !== "test"){ var levels = (requestedItem ==="kanji")? "/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50": "/1,2,3,4,5,6,7,8,9,10"; var xhrk = createCORSRequest("get", "https://www.wanikani.com/api/user/" + APIkey + "/" + requestedItem + levels); if (!isEmpty(xhrk)){ xhrk.onreadystatechange = function() { if (xhrk.readyState == 4){ var kanjiList = handleReadyStateFour(xhrk,requestedItem); if (requestedItem === 'kanji'){ localStorage.setItem('User-KanjiList', JSON.stringify(kanjiList)); scriptLog(kanjiList); //update locks in localStorage //pass kanjilist into this function //(don't shift things through storage unecessarily) refreshLocks(); }else{ var v = kanjiList.length; scriptLog(v + " items found, attempting to import"); while (v--){ setVocItem(kanjiList[v]); } } //------ } }; xhrk.send(); scriptLog("below"); } } else { //dummy server response for testing. setTimeout(function () { var kanjiList = []; scriptLog("creating dummy response"); kanjiList.push({"character": "猫", "srs": "noServerResp"}); var SRS = "apprentice"; //prompt("enter SRS for 子", "guru"); kanjiList.push({"character": "子", "srs": SRS}); kanjiList.push({"character": "品", "srs": "guru"}); kanjiList.push({"character": "供", "srs": "guru"}); kanjiList.push({"character": "本", "srs": "guru"}); kanjiList.push({"character": "聞", "srs": "apprentice"}); kanjiList.push({"character": "人", "srs": "enlightened"}); kanjiList.push({"character": "楽", "srs": "burned"}); kanjiList.push({"character": "相", "srs": "guru"}); kanjiList.push({"character": "卒", "srs": "noMatchWK"}); kanjiList.push({"character": "無", "srs": "noMatchGuppy"}); scriptLog("Server responded with dummy kanjiList: \n"+JSON.stringify(kanjiList)); localStorage.setItem('User-KanjiList', JSON.stringify(kanjiList)); //update locks in localStorage refreshLocks(); }, 10000); } } function getComponents(kanji){ scriptLog("getComponents(kanji)"); //functions: // none //takes in a string and returns an array containing only the kanji characters in the string. var components = []; for (var c = 0; c < kanji.length; c++){ if(/^[\u4e00-\u9faf]+$/.test(kanji[c])) { components.push(kanji[c]); } } return components; } function refreshLocks(){ //functions: // setLocks(srsitem) //scriptLog("refreshLocks()"); if (localStorage.getItem('User-Vocab')) { scriptLog("var srsList = getSrsList();"); var srsList = getSrsList(); scriptLog("var i = srsList.length;"); var i = srsList.length; while(i--){ scriptLog("srsList[i] = setLocks(srsList[i]);"); srsList[i] = setLocks(srsList[i]); scriptLog("setSrsItem(srsList[i]);"); setSrsItem(srsList[i]); } // scriptLog("Setting new locks: "+JSON.stringify(srsList)); }else{ scriptLog("no srs storage found"); } } function getCompKanji(vocab, kanjiList){ scriptLog("getCompKanji(vocab, kanjiList)"); //functions: // getComponents(vocab) var compSRS = []; var kanjiReady = false; //indicates if the kanjiList has been populated var userGuppy = false; //indicated if kanjiList has less than 100 items //has the server responded yet if (kanjiList.length > 0){ scriptLog("kanjiList is > 0"); kanjiReady = true; //is there less than 100 kanji in the response if (kanjiList.length < 100){ scriptLog("kanjiList is < 100"); userGuppy = true; } } //break the item down into its component kanji, discards katakana, hiragana etc var components = getComponents(vocab); scriptLog(vocab+": "+JSON.stringify(components)); //for each kanji character component // this is the outer loop since there will be far less of them than kanjiList for(var i = 0; i < components.length; i++){ var matched = false; //for each kanji returned by the server for(var j=0; j<kanjiList.length; j++){ //if the kanji returned by the server matches the character in the item if (kanjiList[j].character == components[i]){ compSRS[i] = {"kanji": components[i], "srs": kanjiList[j].srs}; matched = true; break; //kanji found: 'i' is its position in item components; 'j' is its postion in the 'kanjiList' server response } } if (matched === false){ // character got all the way through kanjiList without a match. if (kanjiReady){ //was there a server response? if (userGuppy){ //is the user a guppy (kanji probably matches a turtles response) scriptLog("matched=false, kanjiList.length: "+kanjiList.length); compSRS[i] = {"kanji": components[i], "srs": "noMatchGuppy"}; }else{ //user is a turtle, kanji must not have been added to WK (yet) scriptLog("matched=false, kanjiList.length: "+kanjiList.length); compSRS[i] = {"kanji": components[i], "srs": "noMatchWK"}; } }else{ scriptLog("matched=false, kanjiReady=false, noServerResp"); compSRS[i] = {"kanji": components[i], "srs": "noServerResp"}; } } } return compSRS; // compSRS is an array of the kanji with SRS values for each kanji component. // eg. 折り紙: // compSRS = [{"kanji": "折", "srs": "guru"}, {"kanji": "紙", "srs": "apprentice"}] } function createCORSRequest(method, url){ var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr){ xhr.open(method, url, true); } else if (typeof XDomainRequest !== "undefined"){ xhr = new XDomainRequest(); xhr.open(method, url); } else { xhr = null; } return xhr; } function createCSV(JSONstring){ var JSONobject = (typeof JSONstring === 'string') ? jQuery.parseJSON(JSONstring) : JSONstring; var header = []; for (var c=0;c<JSONstring.length; c++){ } window.open('data:text/csv;charset=utf-8,' + encodeURI("")); } $(document).ready(function(){ // Check for file API support. if (window.File && window.FileReader && window.FileList && window.Blob) { } else { alert('The File APIs are not fully supported in this browser.'); } /* * Start the script */ // var beginning = localStorage.getItem('User-KanjiList'); //scriptLog(beginning); //update kanjiList on connection getServerResp(APIkey); scriptInit(); });
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址