您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tool to mark and track individual radicals/kanji/vocabulary
当前为
// ==UserScript== // @name WaniKani Item Marker // @description Tool to mark and track individual radicals/kanji/vocabulary // @namespace irx.wanikani.marker // @include https://www.wanikani.com/* // @version 1 // @copyright 2016, Ingo Radax // @license MIT; http://opensource.org/licenses/MIT // @grant none // ==/UserScript== (function(gobj) { var data; var api_key; var localStoragePrefix = 'ItemMarker_'; //------------------------------------------------------------------- // Main function //------------------------------------------------------------------- function main() { console.log('START - WaniKani Item Marker'); loadData(); updatePage(); console.log('END - WaniKani Item Marker'); } window.addEventListener('load', main, false); window.addEventListener('focus', main, false); //------------------------------------------------------------------- // Update the current page and add the item marker features //------------------------------------------------------------------- function updatePage() { var location = decodeURI(window.location); if (location.endsWith('//www.wanikani.com/') || location.endsWith('/dashboard') || location.endsWith('/dashboard/')) { updateDashboardPage(); } else if (location.endsWith('/review') || location.endsWith('/review/')) { updateReviewPage(); } else if (location.endsWith('/review/session') || location.endsWith('/review/session/')) { updateReviewSessionPage(); } else { var parsedUrl = parseItemUrl(location); if (parsedUrl) { updateItemPage(parsedUrl.type, parsedUrl.name); } } } //------------------------------------------------------------------- // Try to parse the url and detect if it belongs to a single item. // e.g. 'https://www.wanikani.com/level/1/radicals/construction' // will be parsed as 'radicals' and 'construction' //------------------------------------------------------------------- function parseItemUrl(url) { var parsed = /.*\/(radicals|kanji|vocabulary)\/(.+)/.exec(url); if (parsed) { return {type:parsed[1], name:parsed[2]}; } else { return null; } } //------------------------------------------------------------------- // Load item marker data from local storage //------------------------------------------------------------------- function loadData() { data = localStorage.getItem(localStoragePrefix + 'markedItems'); if (data == null) { data = { items:[] }; } else { data = JSON.parse(data); } } //------------------------------------------------------------------- // Save item marker data to local storage //------------------------------------------------------------------- function saveData() { localStorage.setItem(localStoragePrefix + 'markedItems', JSON.stringify(data)); } //------------------------------------------------------------------- // Return the in dex of the given item in the list // returns -1 if item isn't in list //------------------------------------------------------------------- function indexOf(type, name) { return data.items.findIndex(function(item) { return (item.type == type) && (item.name == name); }); } //------------------------------------------------------------------- // Remove all marks //------------------------------------------------------------------- function unmarkAllItems() { loadData(); data.items = []; saveData(); updatePage(); } //------------------------------------------------------------------- // Force refresh of page and data //------------------------------------------------------------------- function forceRefresh() { localStorage.removeItem(localStoragePrefix + 'radicalInfo'); for (var i = 0; i < data.items.length; i++) { if (data.items[i].type == 'radicals') { data.items[i].radical_character = null; } } updatePage(); } //------------------------------------------------------------------- // Unmark a single item //------------------------------------------------------------------- function unmarkItem(type, name) { loadData(); var index = indexOf(type, name); if (index > -1) { data.items.splice(index, 1); saveData(); } updatePage(); } //------------------------------------------------------------------- // Sort the list of marked items //------------------------------------------------------------------- function sortItems() { loadData(); data.items.sort( function(a, b) { if (a.type == 'radicals') { if (b.type != 'radicals') { return -1; } } else if (a.type == 'kanji') { if (b.type == 'radicals') { return 1; } else if (b.type == 'vocabulary') { return -1; } } else { if (b.type != 'vocabulary') { return 1; } } return a.name.localeCompare(b.name); }); saveData(); updatePage(); } //------------------------------------------------------------------- // Mark a single item //------------------------------------------------------------------- function markItem(type, name) { loadData(); var index = indexOf(type, name); if (index === -1) { data.items.push({type:type, name:name, radical_character:null}); saveData(); } updatePage(); } //------------------------------------------------------------------- // Callback if the currentItem changes //------------------------------------------------------------------- function onCurrentItemChanged() { updatePage(); } //------------------------------------------------------------------- // Extends the review session page // - Adds buttons to mark/unmark the current item //------------------------------------------------------------------- function updateReviewSessionPage() { $.jStorage.stopListening('currentItem', onCurrentItemChanged); $.jStorage.listenKeyChange('currentItem', onCurrentItemChanged); var currentItem = $.jStorage.get('currentItem'); var name; var type; if (currentItem.hasOwnProperty('rad')) { type = 'radicals'; name = currentItem.en[0].toLowerCase().replace(/\s/g, "-"); } else if (currentItem.hasOwnProperty('kan')) { type = 'kanji'; name = currentItem.kan; } else if (currentItem.hasOwnProperty('voc')) { type = 'vocabulary'; name = currentItem.voc; } else { return; } //console.log('updateReviewSessionPage - type: ' + type); //console.log('updateReviewSessionPage - name: ' + name); $('#option-mark').remove(); $('#option-unmark').remove(); var item_index = indexOf(type, name); if (item_index === -1) { $('#additional-content ul').append('<li id="option-mark"><span title="Mark this item">Mark</span></li>'); $('#option-mark').on('click', function() { markItem(type, name); }); } else { $('#additional-content ul').append('<li id="option-unmark"><span title="Unmark this item">Unmark</span></li>'); $('#option-unmark').on('click', function() { unmarkItem(type, name); }); } calculateDynamicWidthForReviewPage(); } //------------------------------------------------------------------- // Updates the dynamic with for the review page //------------------------------------------------------------------- function calculateDynamicWidthForReviewPage(){ var liCount = $('#additional-content ul').children().size(); var percentage = 100 / liCount; percentage -= 0.1; cssDynamicWidth = '#additional-content ul li {' + ' width: ' + percentage + '% !important' + '} '; addStyle(cssDynamicWidth); } //------------------------------------------------------------------- // Adds a css to the page //------------------------------------------------------------------- function addStyle(aCss) { var head, style; head = document.getElementsByTagName('head')[0]; if (head) { style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.textContent = aCss; head.appendChild(style); return style; } return null; } //------------------------------------------------------------------- // Extends the review page // - Adds a black/white border around marked/unmarked items displayed on the page //------------------------------------------------------------------- function updateReviewPage() { var query = $('div.active li'); if (query.length == 0) { // Page not completly loaded yet setTimeout(updatePage, 1000); return; } query.each(function(index) { var href = $(this).find('a').attr('href'); var parsedUrl = parseItemUrl(href); if (parsedUrl) { var item_index = indexOf(parsedUrl.type, parsedUrl.name); if (item_index === -1) { $(this).css('border', '3px solid white'); } else { $(this).css('border', '3px solid black'); } } }); } //------------------------------------------------------------------- // Extends the dashboard // - Adds a 'marked items' section // - Adds common buttons and marked items list to section //------------------------------------------------------------------- function updateDashboardPage() { var query = $('section.progression'); if (query.length != 1) { return; } $('#marked_items').remove(); $('section.progression').after('<section id="marked_items" />'); $('#marked_items').append('<h2>Marked items</h2>'); $('#marked_items').append('<p id="marked_items_buttons" />'); addCommonButtons(); addMarkedItemsList(); } //------------------------------------------------------------------- // Extends the an item page // - Adds a 'marked items' section // - Adds common buttons and marked items list to section // - Adds buttons to mark/unmark the current item //------------------------------------------------------------------- function updateItemPage(type, name) { var query = $('section#information'); if (query.length != 1) { return; } $('#marked_items').remove(); $('section#information').after('<section id="marked_items" />'); $('#marked_items').append('<h2>Marked items</h2>'); $('#marked_items').append('<p id="marked_items_buttons" />'); var index = indexOf(type, name); if (index === -1) { var button = $('<button>Mark "' + name + '"</button>'); button.on('click', function() { markItem(type, name); }); $('#marked_items_buttons').append(button) } else { var button = $('<button>Unmark "' + name + '"</button>'); button.on('click', function() { unmarkItem(type, name); }); $('#marked_items_buttons').append(button) } addCommonButtons(); addMarkedItemsList(); } //------------------------------------------------------------------- // Adds common buttons that are used on multiple locations //------------------------------------------------------------------- function addCommonButtons() { if (data.items.length > 0) { var button = $('<button>Unmark all</button>'); button.on('click', function() { unmarkAllItems(); }); $('#marked_items_buttons').append(button) } if (data.items.length > 0) { var button = $('<button>Sort</button>'); button.on('click', function() { sortItems(); }); $('#marked_items_buttons').append(button) } { var button = $('<button>Force refresh</button>'); button.on('click', function() { forceRefresh(); }); $('#marked_items_buttons').append(button) } } //------------------------------------------------------------------- // Adds the list of marked items to the current page //------------------------------------------------------------------- function addMarkedItemsList() { $('#marked_items').append('<span id="marked_items_list" />') if (data.items.length == 0) { $('#marked_items_list').append('<p>No marked items</p>'); } else { var buildItemList = function() { for (var i = 0; i < data.items.length; i++) { var item = data.items[i]; var typeForClass = item.type; if (typeForClass == 'radicals') typeForClass = 'radical'; var itemText = item.name; if (item.type == 'radicals') { if (item.radical_character != '') { itemText = item.radical_character; } else { itemText = '<i class="radical-' + item.name + '"></i>'; } } $('#marked_items_list').append( '<a href="/' + item.type + '/' + item.name + '">' + ' <span class="' + typeForClass + '-icon" lang="ja">' + ' <span style="display: inline-block; margin-top: 0.1em;" class="japanese-font-styling-correction">' + itemText + ' </span>' + ' </span>' + '</a>'); } }; var refetchRadicalInfo = detect_radical_characters(); if (refetchRadicalInfo) { get_api_key() .then(fetch_radical_info) .then(buildItemList); } else { buildItemList(); } } } //------------------------------------------------------------------- // Fetches radical information from the WaniKani API // Some radicals use an image intead of a character. The radical // info helps us determine that. //------------------------------------------------------------------- function fetch_radical_info() { console.log('Calling: "/api/user/' + api_key + '/radicals/"'); return new Promise(function(resolve, reject) { $.getJSON('/api/user/' + api_key + '/radicals/', function(json){ if (json.error && json.error.code === 'user_not_found') { localStorage.removeItem(localStoragePrefix + 'radicalInfo'); location.reload(); reject(); return; } localStorage.setItem(localStoragePrefix + 'radicalInfo', JSON.stringify(json)); detect_radical_characters(); resolve(); }); }); } //------------------------------------------------------------------- // Uses the radical info to determine if a radical uses a character // or an image. //------------------------------------------------------------------- function detect_radical_characters() { var radicalInfo = localStorage.getItem(localStoragePrefix + 'radicalInfo'); if (radicalInfo == null) return true; radicalInfo = JSON.parse(radicalInfo); $(data.items).each(function(i, item) { if ((item.type == 'radicals') && (item.radical_character == null)) { var radical_character = null; $(radicalInfo.requested_information).each(function(j, info){ if (item.name == info.meaning) { if (info.image == null) { radical_character = info.character; } else { radical_character = ''; } } }); console.log('detect_radical_characters: ' + item.name + ' -> ' + radical_character); data.items[i].radical_character = radical_character; } }); saveData(); $(data.items).each(function(i, item) { if ((item.type == 'radicals') && (item.radical_character == null)) { return true; } }); return false; } //------------------------------------------------------------------- // Fetch a document from the server. //------------------------------------------------------------------- function ajax_retry(url, retries, timeout) { retries = retries || 2; timeout = timeout || 3000; function action(resolve, reject) { $.ajax({ url: url, timeout: timeout }) .done(function(data, status){ if (status === 'success') resolve(data); else reject(); }) .fail(function(xhr, status, error){ if (status === 'error' && --retries > 0) action(resolve, reject); else reject(); }); } return new Promise(action); } //------------------------------------------------------------------- // Fetch API key from account page. //------------------------------------------------------------------- function get_api_key() { return new Promise(function(resolve, reject) { api_key = localStorage.getItem(localStoragePrefix + 'apiKey'); if (typeof api_key === 'string' && api_key.length == 32) return resolve(); ajax_retry('/account').then(function(page) { // --[ SUCCESS ]---------------------- // Make sure what we got is a web page. if (typeof page !== 'string') {return reject();} // Extract the user name. page = $(page); // Extract the API key. api_key = page.find('#api-button').parent().find('input').attr('value'); if (typeof api_key !== 'string' || api_key.length !== 32) {return reject();} localStorage.setItem(localStoragePrefix + 'apiKey', api_key); resolve(); },function(result) { // --[ FAIL ]------------------------- reject(new Error('Failed to fetch API key!')); }); }); } }());