WaniKani Item Marker

Tool to mark and track individual radicals/kanji/vocabulary

  1. // ==UserScript==
  2. // @name WaniKani Item Marker
  3. // @description Tool to mark and track individual radicals/kanji/vocabulary
  4. // @namespace irx.wanikani.marker
  5. // @include https://www.wanikani.com/*
  6. // @version 4.0
  7. // @copyright 2017, Ingo Radax
  8. // @license MIT; http://opensource.org/licenses/MIT
  9. // @grant none
  10. // @run-at document-start
  11. // ==/UserScript==
  12.  
  13. var WaniKani = (function() {
  14. var local_storage_prefix = 'wk_toolkit_';
  15. var api_key = null;
  16. var radical_data = null;
  17. var kanji_data = null;
  18. var vocabulary_data = null;
  19.  
  20. var radicals_with_image = [];
  21. function log(msg) {
  22. console.log(msg);
  23. }
  24. function is_on_wanikani() {
  25. return (window.location.host == 'www.wanikani.com');
  26. }
  27. function is_on_dashboard() {
  28. return is_on_wanikani() && ((window.location.pathname == '/dashboard') || (window.location.pathname == '/'));
  29. }
  30. function is_on_review_session_page() {
  31. return is_on_wanikani() && (window.location.pathname == '/review/session');
  32. }
  33. function is_on_review_page() {
  34. return is_on_wanikani() && (window.location.pathname == '/review');
  35. }
  36. function is_on_lesson_session_page() {
  37. return is_on_wanikani() && (window.location.pathname == '/lesson/session');
  38. }
  39. function is_on_lesson_page() {
  40. return is_on_wanikani() && (window.location.pathname == '/lesson');
  41. }
  42. function is_on_lattice_radicals_meaning() {
  43. return is_on_wanikani() && (window.location.pathname == '/lattice/radicals/meaning');
  44. }
  45. function is_on_lattice_radicals_progress() {
  46. return is_on_wanikani() && (window.location.pathname == '/lattice/radicals/progress');
  47. }
  48. function is_on_lattice_kanji_combined() {
  49. return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/combined');
  50. }
  51. function is_on_lattice_kanji_meaning() {
  52. return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/meaning');
  53. }
  54. function is_on_lattice_kanji_reading() {
  55. return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/reading');
  56. }
  57. function is_on_lattice_kanji_progress() {
  58. return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/status');
  59. }
  60. function is_on_lattice_vocabulary_combined() {
  61. return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/combined');
  62. }
  63. function is_on_lattice_vocabulary_meaning() {
  64. return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/meaning');
  65. }
  66. function is_on_lattice_vocabulary_reading() {
  67. return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/reading');
  68. }
  69. function is_on_lattice_vocabulary_progress() {
  70. return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/status');
  71. }
  72. //-------------------------------------------------------------------
  73. // Try to parse the url and detect if it belongs to a single item.
  74. // e.g. 'https://www.wanikani.com/level/1/radicals/construction'
  75. // will be parsed as 'radicals' and 'construction'
  76. //-------------------------------------------------------------------
  77. function parse_item_url(url) {
  78. if ((url == null) || (url == undefined)) {
  79. return null;
  80. }
  81. if (url.indexOf('/lattice/') > -1) {
  82. return null;
  83. }
  84. url = decodeURI(url);
  85. var parsed = /.*\/(radicals|kanji|vocabulary)\/(.+)/.exec(url);
  86. if (parsed) {
  87. return {type:parsed[1], name:parsed[2]};
  88. }
  89. else {
  90. return null;
  91. }
  92. }
  93. function reset_radical_data(resetLocalStorage) {
  94. radical_data = null;
  95. if (resetLocalStorage) {
  96. localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
  97. localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
  98. }
  99. }
  100. function reset_kanji_data(resetLocalStorage) {
  101. kanji_data = null;
  102. if (resetLocalStorage) {
  103. localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
  104. localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
  105. }
  106. }
  107. function reset_vocabulary_data(resetLocalStorage) {
  108. vocabulary_data = null;
  109. if (resetLocalStorage) {
  110. localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
  111. localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
  112. }
  113. }
  114. function clear_local_storage() {
  115. localStorage.removeItem(local_storage_prefix + 'last_review_time');
  116. localStorage.removeItem(local_storage_prefix + 'next_review_time');
  117. localStorage.removeItem(local_storage_prefix + 'last_unlock_time');
  118. localStorage.removeItem(local_storage_prefix + 'api_key');
  119. localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
  120. localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
  121. localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
  122. localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
  123. localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
  124. localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
  125. }
  126. function track_times() {
  127. if (is_on_review_session_page()) {
  128. localStorage.setItem(local_storage_prefix + 'last_review_time', now());
  129. var lastUnlockTime = new Date($('.recent-unlocks time:nth(0)').attr('datetime'))/1000;
  130. localStorage.setItem(local_storage_prefix + 'last_unlock_time', now());
  131. }
  132. if (is_on_dashboard()) {
  133. var next_review = Number($('.review-status .timeago').attr('datetime'));
  134. // Workaround for "WaniKani Real Times" script, which deletes the element we were looking for above.
  135. if (isNaN(next_review)) {
  136. next_review = Number($('.review-status time1').attr('datetime'));
  137. // Conditional divide-by-1000, in case someone fixed this error in Real Times script.
  138. if (next_review > 10000000000) next_review /= 1000;
  139. }
  140. localStorage.setItem(local_storage_prefix + 'next_review_time', next_review);
  141. }
  142. }
  143. function get_last_review_time() {
  144. return Number(localStorage.getItem(local_storage_prefix + 'last_review_time') || 0);
  145. }
  146. function get_next_review_time() {
  147. return Number(localStorage.getItem(local_storage_prefix + 'next_review_time') || 0);
  148. }
  149. function get_last_unlock_time() {
  150. return Number(localStorage.getItem(local_storage_prefix + 'last_unlock_time') || 0);
  151. }
  152.  
  153. function now() {
  154. return Math.floor(new Date() / 1000);
  155. }
  156. function ajax_retry(url, retries, timeout) {
  157. retries = retries || 2;
  158. timeout = timeout || 3000;
  159. function action(resolve, reject) {
  160. $.ajax({
  161. url: url,
  162. timeout: timeout
  163. })
  164. .done(function(data, status){
  165. if (status === 'success')
  166. resolve(data);
  167. else
  168. reject();
  169. })
  170. .fail(function(xhr, status, error){
  171. if (status === 'error' && --retries > 0)
  172. action(resolve, reject);
  173. else
  174. reject();
  175. });
  176. }
  177. return new Promise(action);
  178. }
  179.  
  180. function query_page_radicals_with_images() {
  181. return new Promise(function(resolve, reject) {
  182. ajax_retry('/lattice/radicals/meaning').then(function(page) {
  183. if (typeof page !== 'string') {return reject();}
  184. page = $(page);
  185. radicals_with_image = [];
  186. page.find('li').each(function(i, item) {
  187. var attr = $(this).attr('id');
  188. if (attr && attr.startsWith('radical-')) {
  189. var radicalChar = $(this).text();
  190. var href = $(this).find('a').attr('href');
  191. var parsedHref = WaniKani.parse_item_url(href);
  192. var image = $(this).find('img');
  193. var imageSrc = '';
  194. if (image.length > 0) {
  195. imageSrc = image.attr('src');
  196. }
  197. radicals_with_image.push({ name: parsedHref.name, character: radicalChar, image: imageSrc });
  198. }
  199. });
  200. resolve();
  201.  
  202. },function(result) {
  203. reject(new Error('Failed to query page!'));
  204. });
  205. });
  206. }
  207. function get_radicals_with_image() {
  208. return radicals_with_image;
  209. }
  210. function get_api_key() {
  211. return new Promise(function(resolve, reject) {
  212. api_key = localStorage.getItem(local_storage_prefix + 'api_key');
  213. if (typeof api_key === 'string' && api_key.length == 32) {
  214. log("Already having API key");
  215. return resolve();
  216. }
  217. log("Loading API key");
  218. ajax_retry('/account').then(function(page) {
  219. log("Loading API key ... SUCCESS");
  220. // --[ SUCCESS ]----------------------
  221. // Make sure what we got is a web page.
  222. if (typeof page !== 'string') {return reject();}
  223.  
  224. // Extract the user name.
  225. page = $(page);
  226. // Extract the API key.
  227. api_key = page.find('#api-button').parent().find('input').attr('value');
  228. if (typeof api_key !== 'string' || api_key.length !== 32) {return reject();}
  229.  
  230. localStorage.setItem(local_storage_prefix + 'api_key', api_key);
  231. resolve();
  232.  
  233. },function(result) {
  234. log("Loading API key ... ERROR");
  235. // --[ FAIL ]-------------------------
  236. reject(new Error('Failed to fetch API key!'));
  237. });
  238. });
  239. }
  240. function call_api_user_radicals() {
  241. return new Promise(function(resolve, reject) {
  242. log("Calling API: User radicals");
  243. $.getJSON('/api/user/' + api_key + '/radicals/', function(json){
  244. if (json.error && json.error.code === 'user_not_found') {
  245. log("Calling API: User radicals ... ERROR")
  246. reset_radical_data(true);
  247. location.reload();
  248. reject();
  249. return;
  250. }
  251.  
  252. log("Calling API: User radicals ... SUCCESS");
  253. localStorage.setItem(local_storage_prefix + 'api_user_radicals', JSON.stringify(json));
  254. localStorage.setItem(local_storage_prefix + 'api_user_radicals_fetch_time', now());
  255. radical_data = json;
  256. resolve();
  257. });
  258. });
  259. }
  260. function call_api_user_kanji() {
  261. return new Promise(function(resolve, reject) {
  262. log("Calling API: User kanji");
  263. $.getJSON('/api/user/' + api_key + '/kanji/', function(json){
  264. if (json.error && json.error.code === 'user_not_found') {
  265. log("Calling API: User kanji ... ERROR")
  266. reset_kanji_data(true);
  267. location.reload();
  268. reject();
  269. return;
  270. }
  271.  
  272. log("Calling API: User kanji ... SUCCESS");
  273. localStorage.setItem(local_storage_prefix + 'api_user_kanji', JSON.stringify(json));
  274. localStorage.setItem(local_storage_prefix + 'api_user_kanji_fetch_time', now());
  275. kanji_data = json;
  276. resolve();
  277. });
  278. });
  279. }
  280. function call_api_user_vocabulary() {
  281. return new Promise(function(resolve, reject) {
  282. log("Calling API: User vocabulary");
  283. $.getJSON('/api/user/' + api_key + '/vocabulary/', function(json){
  284. if (json.error && json.error.code === 'user_not_found') {
  285. log("Calling API: User vocabulary ... ERROR")
  286. reset_vocabulary_data(true);
  287. location.reload();
  288. reject();
  289. return;
  290. }
  291.  
  292. log("Calling API: User vocabulary ... SUCCESS");
  293. localStorage.setItem(local_storage_prefix + 'api_user_vocabulary', JSON.stringify(json));
  294. localStorage.setItem(local_storage_prefix + 'api_user_vocabulary_fetch_time', now());
  295. vocabulary_data = json;
  296. resolve();
  297. });
  298. });
  299. }
  300. function get_last_fetch_time_api_user_radicals() {
  301. return Number(localStorage.getItem(local_storage_prefix + 'api_user_radicals_fetch_time'));
  302. }
  303. function get_last_fetch_time_api_user_kanji() {
  304. return Number(localStorage.getItem(local_storage_prefix + 'api_user_kanji_fetch_time'));
  305. }
  306. function get_last_fetch_time_api_user_vocabulary() {
  307. return Number(localStorage.getItem(local_storage_prefix + 'api_user_vocabulary_fetch_time'));
  308. }
  309. function load_radical_data() {
  310. var next_review_time = get_next_review_time();
  311. var last_review_time = get_last_review_time();
  312. var last_unlock_time = get_last_unlock_time();
  313. var last_fetch_time = get_last_fetch_time_api_user_radicals();
  314. if ((last_fetch_time <= last_unlock_time) ||
  315. (last_fetch_time <= last_review_time) ||
  316. ((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
  317. log("Clearing previous fetched radical data");
  318. radical_data = null;
  319. localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
  320. localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
  321. }
  322. if (radical_data == null) {
  323. var stringified = localStorage.getItem(local_storage_prefix + 'api_user_radicals');
  324. if (stringified != null) {
  325. log("Radical data loaded from local storage");
  326. radical_data = JSON.parse(stringified);
  327. }
  328. }
  329. if (radical_data != null) {
  330. log("Radical data already loaded");
  331. return Promise.resolve();
  332. }
  333. return new Promise(function(resolve, reject) {
  334. get_api_key()
  335. .then(call_api_user_radicals)
  336. .then(function() {
  337. resolve();
  338. });
  339. });
  340. }
  341. function load_kanji_data() {
  342. var next_review_time = get_next_review_time();
  343. var last_review_time = get_last_review_time();
  344. var last_unlock_time = get_last_unlock_time();
  345. var last_fetch_time = get_last_fetch_time_api_user_kanji();
  346. if ((last_fetch_time <= last_unlock_time) ||
  347. (last_fetch_time <= last_review_time) ||
  348. ((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
  349. log("Clearing previous fetched kanji data");
  350. kanji_data = null;
  351. localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
  352. localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
  353. }
  354. if (kanji_data == null) {
  355. var stringified = localStorage.getItem(local_storage_prefix + 'api_user_kanji');
  356. if (stringified != null) {
  357. log("Kanji data loaded from local storage");
  358. kanji_data = JSON.parse(stringified);
  359. }
  360. }
  361. if (kanji_data != null) {
  362. log("Kanji data already loaded");
  363. return Promise.resolve();
  364. }
  365. return new Promise(function(resolve, reject) {
  366. get_api_key()
  367. .then(call_api_user_kanji)
  368. .then(function() {
  369. resolve();
  370. });
  371. });
  372. }
  373. function load_vocabulary_data() {
  374. var next_review_time = get_next_review_time();
  375. var last_review_time = get_last_review_time();
  376. var last_unlock_time = get_last_unlock_time();
  377. var last_fetch_time = get_last_fetch_time_api_user_vocabulary();
  378. if ((last_fetch_time <= last_unlock_time) ||
  379. (last_fetch_time <= last_review_time) ||
  380. ((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
  381. log("Clearing previous fetched vocabulary data");
  382. vocabulary_data = null;
  383. localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
  384. localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
  385. }
  386. if (vocabulary_data == null) {
  387. var stringified = localStorage.getItem(local_storage_prefix + 'api_user_vocabulary');
  388. if (stringified != null) {
  389. log("Vocabulary data loaded from local storage");
  390. vocabulary_data = JSON.parse(stringified);
  391. }
  392. }
  393. if (vocabulary_data != null) {
  394. log("Vocabulary data already loaded");
  395. return Promise.resolve();
  396. }
  397. return new Promise(function(resolve, reject) {
  398. get_api_key()
  399. .then(call_api_user_vocabulary)
  400. .then(function() {
  401. resolve();
  402. });
  403. });
  404. }
  405. function get_radical_data() {
  406. return radical_data;
  407. }
  408. function get_kanji_data() {
  409. return kanji_data;
  410. }
  411. function get_vocabulary_data() {
  412. return vocabulary_data;
  413. }
  414. function find_radical(meaning) {
  415. if (radical_data == null) {
  416. return null;
  417. }
  418. var numRadicals = radical_data.requested_information.length;
  419. for (var i = 0; i < numRadicals; i++) {
  420. if (radical_data.requested_information[i].meaning == meaning) {
  421. return radical_data.requested_information[i];
  422. }
  423. }
  424. return null;
  425. }
  426. function find_kanji(character) {
  427. if (kanji_data == null) {
  428. return null;
  429. }
  430. var numKanji = kanji_data.requested_information.length;
  431. for (var i = 0; i < numKanji; i++) {
  432. if (kanji_data.requested_information[i].character == character) {
  433. return kanji_data.requested_information[i];
  434. }
  435. }
  436. return null;
  437. }
  438. function find_vocabulary(character) {
  439. if (vocabulary_data == null) {
  440. return null;
  441. }
  442. var numVocabulary = vocabulary_data.requested_information.general.length;
  443. for (var i = 0; i < numVocabulary; i++) {
  444. if (vocabulary_data.requested_information.general[i].character == character) {
  445. return vocabulary_data.requested_information.general[i];
  446. }
  447. }
  448. return null;
  449. }
  450. function find_item(type, name) {
  451. if (type == 'radicals') {
  452. return find_radical(name);
  453. }
  454. else if(type == 'kanji') {
  455. return find_kanji(name);
  456. }
  457. else if(type == 'vocabulary') {
  458. return find_vocabulary(name);
  459. }
  460. else {
  461. return null;
  462. }
  463. }
  464. return {
  465. is_on_wanikani: is_on_wanikani,
  466. is_on_dashboard: is_on_dashboard,
  467. is_on_review_session_page: is_on_review_session_page,
  468. is_on_review_page: is_on_review_page,
  469. is_on_lesson_session_page: is_on_lesson_session_page,
  470. is_on_lesson_page: is_on_lesson_page,
  471. is_on_lattice_radicals_meaning: is_on_lattice_radicals_meaning,
  472. is_on_lattice_radicals_progress: is_on_lattice_radicals_progress,
  473. is_on_lattice_kanji_combined: is_on_lattice_kanji_combined,
  474. is_on_lattice_kanji_meaning: is_on_lattice_kanji_meaning,
  475. is_on_lattice_kanji_reading: is_on_lattice_kanji_reading,
  476. is_on_lattice_kanji_progress: is_on_lattice_kanji_progress,
  477. is_on_lattice_vocabulary_combined: is_on_lattice_vocabulary_combined,
  478. is_on_lattice_vocabulary_meaning: is_on_lattice_vocabulary_meaning,
  479. is_on_lattice_vocabulary_reading: is_on_lattice_vocabulary_reading,
  480. is_on_lattice_vocabulary_progress: is_on_lattice_vocabulary_progress,
  481. query_page_radicals_with_images: query_page_radicals_with_images,
  482. get_radicals_with_image: get_radicals_with_image,
  483. parse_item_url: parse_item_url,
  484. reset_radical_data: reset_radical_data,
  485. reset_kanji_data: reset_kanji_data,
  486. reset_vocabulary_data: reset_vocabulary_data,
  487. clear_local_storage: clear_local_storage,
  488. track_times: track_times,
  489. get_last_review_time: get_last_review_time,
  490. get_next_review_time: get_next_review_time,
  491. load_radical_data: load_radical_data,
  492. get_radical_data: get_radical_data,
  493. find_radical: find_radical,
  494. load_kanji_data: load_kanji_data,
  495. get_kanji_data: get_kanji_data,
  496. find_kanji: find_kanji,
  497. load_vocabulary_data: load_vocabulary_data,
  498. get_vocabulary_data: get_vocabulary_data,
  499. find_vocabulary: find_vocabulary,
  500. find_item: find_item,
  501. };
  502. })();
  503.  
  504. var UI = (function() {
  505. function addStyle(aCss) {
  506. var head, style;
  507. head = document.getElementsByTagName('head')[0];
  508. if (head) {
  509. style = document.createElement('style');
  510. style.setAttribute('type', 'text/css');
  511. style.textContent = aCss;
  512. head.appendChild(style);
  513. return style;
  514. }
  515. return null;
  516. }
  517. function initCss() {
  518. var css =
  519. '#item_marker {' +
  520. ' display:none;' +
  521. '}' +
  522. '#item_marker h3 {' +
  523. ' height: 24px;' +
  524. ' margin-top: 10px;' +
  525. ' margin-bottom: 0px;' +
  526. ' padding: 5px 20px;' +
  527. ' border-radius: 5px 5px 0 0;' +
  528. ' background-color: seagreen;' +
  529. ' color: white;' +
  530. ' text-shadow: none;' +
  531. '}' +
  532. '#item_marker section {' +
  533. ' background-color: lightgrey;' +
  534. '}' +
  535. '#item_marker table {' +
  536. '}' +
  537. '#item_marker td {' +
  538. ' padding: 2px 8px;' +
  539. '}' +
  540. '#item_marker .close_button {' +
  541. ' float: right;' +
  542. ' height: 24px;' +
  543. '}'
  544. ;
  545. addStyle(css);
  546. }
  547. function buildIdAttr(id) {
  548. if (id && id != '')
  549. return 'id="' + id + '"';
  550. return '';
  551. }
  552. function addOnChangeListener(id, listener) {
  553. $('#' + id).off('change');
  554. $('#' + id).on('change', listener);
  555. }
  556. function addOnClickListener(id, listener) {
  557. $('#' + id).off('click');
  558. $('#' + id).on('click', listener);
  559. }
  560. function buildWindow(id, title) {
  561. var html =
  562. '<div ' + buildIdAttr(id) + ' class="container">' +
  563. '<div class="row">' +
  564. '<div class="span12" >' +
  565. '<section id="' + id + '_body">' +
  566. '<h3>' +
  567. title +
  568. '<button class="close_button" " ' + buildIdAttr(id + '_close_btn') + '>Close</button>' +
  569. '</h3>' +
  570. '</section>' +
  571. '</div>' +
  572. '</div>' +
  573. '</div>'
  574. ;
  575. return html;
  576. }
  577. function buildTable(id) {
  578. var html =
  579. '<table ' + buildIdAttr(id) + '>' +
  580. '<tbody ' + buildIdAttr(id + '_body') + '>' +
  581. '</tbody>' +
  582. '</table>';
  583. return html;
  584. }
  585. function buildTableRow(id, column1, column2) {
  586. var html =
  587. '<tr' + buildIdAttr(id) + '>' +
  588. '<td>' + column1 + '</td>' +
  589. '<td>' + column2 + '</td>' +
  590. '</tr>';
  591. return html;
  592. }
  593. function buildSelection(id, tooltip) {
  594. var html =
  595. '<select ' + buildIdAttr(id) + ' class="input" name="' + id + '" title="' + tooltip + '" />';
  596. return html;
  597. }
  598. function addSelectionOption(selectId, value, label, selected) {
  599. $('#' + selectId).append(
  600. '<option value="' + value + '" ' + (selected ? 'selected' : '') + '>' +
  601. label +
  602. '</option>');
  603. }
  604. function buildCheckBox(id, checked) {
  605. var html =
  606. '<input ' + buildIdAttr(id) + ' type="checkbox" ' + (checked ? 'checked' : '') + '>';
  607. return html;
  608. }
  609. return {
  610. initCss: initCss,
  611. buildWindow: buildWindow,
  612. buildTable: buildTable,
  613. buildTableRow: buildTableRow,
  614. buildCheckBox: buildCheckBox,
  615. buildSelection: buildSelection,
  616. addSelectionOption: addSelectionOption,
  617. addOnChangeListener: addOnChangeListener,
  618. addOnClickListener: addOnClickListener,
  619. };
  620. })();
  621.  
  622. (function(gobj) {
  623. var data = {
  624. lists: [
  625. {
  626. name: 'A',
  627. items:[],
  628. settings: {
  629. markedItemsBorderColor: 'black',
  630. toggleMarksWithLinks: false,
  631. showReviewWarning: false,
  632. addDelayBeforeAnswerIsPossible: false,
  633. }
  634. },
  635. {
  636. name: 'B',
  637. items:[],
  638. settings: {
  639. markedItemsBorderColor: 'red',
  640. toggleMarksWithLinks: false,
  641. showReviewWarning: false,
  642. addDelayBeforeAnswerIsPossible: false,
  643. }
  644. },
  645. {
  646. name: 'C',
  647. items:[],
  648. settings: {
  649. markedItemsBorderColor: 'orange',
  650. toggleMarksWithLinks: false,
  651. showReviewWarning: false,
  652. addDelayBeforeAnswerIsPossible: false,
  653. }
  654. },
  655. {
  656. name: 'D',
  657. items:[],
  658. settings: {
  659. markedItemsBorderColor: 'yellow',
  660. toggleMarksWithLinks: false,
  661. showReviewWarning: false,
  662. addDelayBeforeAnswerIsPossible: false,
  663. }
  664. },
  665. {
  666. name: 'E',
  667. items:[],
  668. settings: {
  669. markedItemsBorderColor: 'cyan',
  670. toggleMarksWithLinks: false,
  671. showReviewWarning: false,
  672. addDelayBeforeAnswerIsPossible: false,
  673. }
  674. }
  675. ],
  676. settings: {
  677. unmarkedItemsBorderColor: 'no_border',
  678. }};
  679. var dataVersion = 2;
  680. var settingsWindowAdded = false;
  681. var dropDownMenuExtended = false;
  682. var availableBorderColors = [
  683. { value: 'no_border', label: 'No border' },
  684. { value: 'black', label: 'Black' },
  685. { value: 'darkgrey', label: 'Dark Grey' },
  686. { value: 'lightgrey', label: 'Light Grey' },
  687. { value: 'white', label: 'White' },
  688. { value: 'red', label: 'Red' },
  689. { value: 'orange', label: 'Orange' },
  690. { value: 'yellow', label: 'Yellow' },
  691. { value: 'green', label: 'Green' },
  692. { value: 'blue', label: 'Blue' },
  693. { value: 'indigo', label: 'Indigo' },
  694. { value: 'violet', label: 'Violet' },
  695. { value: 'cyan', label: 'Cyan' }
  696. ];
  697. var localStoragePrefix = 'ItemMarker_';
  698. var pageUpdateIsLocked = false;
  699. var lastSeenItem = 0;
  700. var lastSeenQuestionType = '';
  701. //-------------------------------------------------------------------
  702. // Main function
  703. //-------------------------------------------------------------------
  704. function main() {
  705. console.log('START - WaniKani Item Marker');
  706. loadData();
  707. WaniKani.track_times();
  708. extendDropDownMenu();
  709. updatePage();
  710. console.log('END - WaniKani Item Marker');
  711. }
  712. window.addEventListener('load', main, false);
  713. //window.addEventListener('focus', main, false);
  714. function extendDropDownMenu() {
  715. if (dropDownMenuExtended) {
  716. return;
  717. }
  718. $('<li><a href="#item_marker">Item Marker</a></li>')
  719. .insertBefore($('.account .dropdown-menu .nav-header:eq(1)'))
  720. .on('click', toggleSettingsWindow);
  721. dropDownMenuExtended = true;
  722. }
  723. function buildSettingsWindow() {
  724. UI.initCss();
  725. var html;
  726. html = UI.buildWindow('item_marker', 'Item Marker');
  727. $(html).insertAfter($('.navbar'));
  728. html = UI.buildTable('item_marker_settings');
  729. console.log(html);
  730. $('#item_marker_body').append(html);
  731. html = UI.buildTableRow(
  732. '',
  733. '<b>General settings</b>',
  734. '');
  735. $('#item_marker_settings_body').append(html);
  736. html = UI.buildTableRow(
  737. '',
  738. 'Border color for unmarked items',
  739. UI.buildSelection(
  740. 'border_color_unmarked_items',
  741. 'Select the border color for unmarked items'));
  742. $('#item_marker_settings_body').append(html);
  743. for (var i = 0; i < availableBorderColors.length; i++) {
  744. UI.addSelectionOption('border_color_unmarked_items',
  745. availableBorderColors[i].value,
  746. availableBorderColors[i].label,
  747. availableBorderColors[i].value == data.settings.unmarkedItemsBorderColor);
  748. }
  749. UI.addOnChangeListener('border_color_unmarked_items',
  750. function()
  751. {
  752. loadData();
  753. data.settings.unmarkedItemsBorderColor = $('#border_color_unmarked_items' + listIndex).val();
  754. saveData();
  755. updateLinks();
  756. });
  757. $(data.lists).each(function(listIndex, list) {
  758. html = UI.buildTableRow(
  759. '',
  760. '<b>Settings of List ' + list.name + '</b>',
  761. '');
  762. $('#item_marker_settings_body').append(html);
  763. html = UI.buildTableRow(
  764. '',
  765. 'Border color for marked items',
  766. UI.buildSelection(
  767. 'border_color_marked_items' + listIndex,
  768. 'Select the border color for marked items'));
  769. $('#item_marker_settings_body').append(html);
  770. for (var i = 0; i < availableBorderColors.length; i++) {
  771. UI.addSelectionOption('border_color_marked_items' + listIndex,
  772. availableBorderColors[i].value,
  773. availableBorderColors[i].label,
  774. availableBorderColors[i].value == list.settings.markedItemsBorderColor);
  775. }
  776. html = UI.buildTableRow(
  777. '',
  778. 'Use links to mark/unmark items',
  779. UI.buildCheckBox(
  780. 'use_links_to_toggle_items' + listIndex,
  781. list.settings.toggleMarksWithLinks));
  782. $('#item_marker_settings_body').append(html);
  783. html = UI.buildTableRow(
  784. '',
  785. 'Show warning during reviews',
  786. UI.buildCheckBox(
  787. 'show_review_warning' + listIndex,
  788. list.settings.showReviewWarning));
  789. $('#item_marker_settings_body').append(html);
  790. html = UI.buildTableRow(
  791. '',
  792. 'Add 30 second delay before review answer is possible',
  793. UI.buildCheckBox(
  794. 'add_delay_before_answer_is_possible' + listIndex,
  795. list.settings.addDelayBeforeAnswerIsPossible));
  796. $('#item_marker_settings_body').append(html);
  797. UI.addOnChangeListener('border_color_marked_items' + listIndex,
  798. function()
  799. {
  800. loadData();
  801. data.lists[listIndex].settings.markedItemsBorderColor = $('#border_color_marked_items' + listIndex).val();
  802. saveData();
  803. updateLinks();
  804. });
  805. UI.addOnChangeListener('use_links_to_toggle_items' + listIndex,
  806. function()
  807. {
  808. loadData();
  809. data.lists[listIndex].settings.toggleMarksWithLinks = $('#use_links_to_toggle_items' + listIndex).is(':checked');
  810. saveData();
  811. updateLinks();
  812. });
  813. UI.addOnChangeListener('show_review_warning' + listIndex,
  814. function()
  815. {
  816. loadData();
  817. data.lists[listIndex].settings.showReviewWarning = $('#show_review_warning' + listIndex).is(':checked');
  818. saveData();
  819. });
  820. UI.addOnChangeListener('add_delay_before_answer_is_possible' + listIndex,
  821. function()
  822. {
  823. loadData();
  824. data.lists[listIndex].settings.addDelayBeforeAnswerIsPossible = $('#add_delay_before_answer_is_possible' + listIndex).is(':checked');
  825. saveData();
  826. });
  827. UI.addOnClickListener('item_marker_close_btn',
  828. function(e)
  829. {
  830. toggleSettingsWindow(e);
  831. });
  832. });
  833. settingsWindowAdded = true;
  834. }
  835.  
  836. function toggleSettingsWindow(e) {
  837. if (e !== undefined) e.preventDefault();
  838.  
  839. // Add the manager if not already.
  840. if (!settingsWindowAdded) buildSettingsWindow();
  841.  
  842. $('#item_marker').slideToggle();
  843. $('html, body').animate({scrollTop: 0}, 800);
  844. }
  845. //-------------------------------------------------------------------
  846. // Update the current page and add the item marker features
  847. //-------------------------------------------------------------------
  848. function updatePage() {
  849. if (pageUpdateIsLocked) {
  850. return;
  851. }
  852. if (WaniKani.is_on_dashboard()) {
  853. updateDashboardPage();
  854. }
  855. else if (WaniKani.is_on_review_page() || WaniKani.is_on_lesson_page()) {
  856. updateReviewAndLessonPage();
  857. }
  858. else if (WaniKani.is_on_review_session_page()) {
  859. updateReviewSessionPage();
  860. }
  861. else {
  862. var location = decodeURI(window.location);
  863. var parsedUrl = WaniKani.parse_item_url(location);
  864. if (parsedUrl) {
  865. updateItemPage(parsedUrl.type, parsedUrl.name);
  866. }
  867. }
  868. updateLinks();
  869. }
  870. function updateLinks() {
  871. $('a').each(function(i, item) {
  872. var href = $(this).attr('href');
  873. updateLinkFunction(href, $(this));
  874. updateItemBorder(href, $(this));
  875. });
  876. $('ul.alt-character-list a').each(function(i, item) {
  877. var href = $(this).attr('href');
  878. var parsedUrl = WaniKani.parse_item_url(href);
  879. if (parsedUrl) {
  880. $(this).css('box-shadow', '');
  881. updateItemBorder(href, $(this).parent('li'));
  882. }
  883. });
  884. $(data.lists).each(function(listIndex, list) {
  885. $('#marked_items_list' + listIndex + ' a').each(function(i, item) {
  886. var href = $(this).attr('href');
  887. var parsedUrl = WaniKani.parse_item_url(href);
  888. if (parsedUrl) {
  889. $(this).css('box-shadow', '');
  890. updateItemBorder(href, $(this).children('span:nth(0)'));
  891. }
  892. });
  893. });
  894. }
  895. function updateLinkFunction(url, htmlElem) {
  896. var parsedUrl = WaniKani.parse_item_url(url);
  897. if (parsedUrl) {
  898. htmlElem.off('click');
  899. $(data.lists).each(function(listIndex, list) {
  900. if (list.settings.toggleMarksWithLinks) {
  901. htmlElem.on('click',
  902. function(e)
  903. {
  904. e.preventDefault();
  905. toggleItemByUri(listIndex, url);
  906. });
  907. }
  908. });
  909. }
  910. }
  911. function updateItemBorder(url, htmlElem) {
  912. var parsedUrl = WaniKani.parse_item_url(url);
  913. if (parsedUrl) {
  914. var marked = false;
  915. var boxShadow = '';
  916. var shadowWidth = 2;
  917. $(data.lists).each(function(listIndex, list) {
  918. var item_index = indexOf(listIndex, parsedUrl.type, parsedUrl.name);
  919. if (item_index !== -1) {
  920. marked = true;
  921. if (list.settings.markedItemsBorderColor != 'no_border') {
  922. if (boxShadow == '') {
  923. boxShadow = 'inset 0 0 0 ' + shadowWidth + 'px ' + list.settings.markedItemsBorderColor;
  924. }
  925. else {
  926. boxShadow = boxShadow + ', inset 0 0 0 ' + shadowWidth + 'px ' + list.settings.markedItemsBorderColor;
  927. }
  928. shadowWidth = shadowWidth + 2;
  929. }
  930. }
  931. });
  932. if (marked) {
  933. htmlElem.css('box-shadow', boxShadow);
  934. }
  935. else {
  936. if (data.settings.unmarkedItemsBorderColor != 'no_border') {
  937. htmlElem.css('box-shadow', 'inset 0 0 0 2px ' + data.settings.unmarkedItemsBorderColor);
  938. }
  939. else {
  940. htmlElem.css('box-shadow', '');
  941. }
  942. }
  943. }
  944. }
  945. //-------------------------------------------------------------------
  946. // Load item marker data from local storage
  947. //-------------------------------------------------------------------
  948. function loadData() {
  949. var storedData = localStorage.getItem(localStoragePrefix + 'markedItems_v' + data.version);
  950. if (storedData != null) {
  951. console.log('Read current version ' + data.version);
  952. storedData = JSON.parse(storedData);
  953. data = storedData;
  954. return;
  955. }
  956. storedData = localStorage.getItem(localStoragePrefix + 'markedItems'); // v1
  957. if (storedData != null) {
  958. console.log('Read old version 1');
  959. storedData = JSON.parse(storedData);
  960. data.lists[0].items = storedData.items;
  961. var storedSettings = localStorage.getItem(localStoragePrefix + 'settings');
  962. if (storedSettings) {
  963. storedSettings = JSON.parse(storedSettings);
  964. data.lists[0].settings = storedSettings;
  965. }
  966. return;
  967. }
  968. }
  969.  
  970. //-------------------------------------------------------------------
  971. // Save item marker data to local storage
  972. //-------------------------------------------------------------------
  973. function saveData() {
  974. localStorage.setItem(localStoragePrefix + 'markedItems_v' + data.version, JSON.stringify(data));
  975. }
  976.  
  977. //-------------------------------------------------------------------
  978. // Return the index of the given item in the list
  979. // returns -1 if item isn't in list
  980. //-------------------------------------------------------------------
  981. function indexOf(listIndex, type, name) {
  982. return data.lists[listIndex].items.findIndex(function(item) { return (item.type == type) && (item.name == name); });
  983. }
  984.  
  985. //-------------------------------------------------------------------
  986. // Remove all marks
  987. //-------------------------------------------------------------------
  988. function unmarkAllItems(listIndex) {
  989. loadData();
  990. data.lists[listIndex].items = [];
  991. saveData();
  992. updatePage();
  993. }
  994.  
  995. //-------------------------------------------------------------------
  996. // Force refresh of page and data
  997. //-------------------------------------------------------------------
  998. function forceRefresh() {
  999. WaniKani.reset_radical_data(true);
  1000. $(data.lists).each(function(listIndex, list) {
  1001. for (var i = 0; i < list.items.length; i++) {
  1002. if (list.items[i].type == 'radicals') {
  1003. list.items[i].radical_character = null;
  1004. list.items[i].radical_image = null;
  1005. }
  1006. }
  1007. });
  1008. updatePage();
  1009. }
  1010. //-------------------------------------------------------------------
  1011. // Unmark a single item
  1012. //-------------------------------------------------------------------
  1013. function unmarkItem(listIndex, type, name) {
  1014. loadData();
  1015. var index = indexOf(listIndex, type, name);
  1016. if (index > -1) {
  1017. data.lists[listIndex].items.splice(index, 1);
  1018. saveData();
  1019. }
  1020. updatePage();
  1021. }
  1022. function toggleItemByUri(listIndex, uri) {
  1023. var parsedUrl = WaniKani.parse_item_url(uri);
  1024. if (parsedUrl) {
  1025. var index = indexOf(listIndex, parsedUrl.type, parsedUrl.name);
  1026. if (index > -1) {
  1027. unmarkItem(listIndex, parsedUrl.type, parsedUrl.name);
  1028. }
  1029. else {
  1030. markItem(listIndex, parsedUrl.type, parsedUrl.name);
  1031. }
  1032. }
  1033. }
  1034.  
  1035. //-------------------------------------------------------------------
  1036. // Sort the list of marked items
  1037. //-------------------------------------------------------------------
  1038. function sortItems(listIndex) {
  1039. loadData();
  1040. data.lists[listIndex].items.sort(
  1041. function(a, b) {
  1042. if (a.type == 'radicals') {
  1043. if (b.type != 'radicals') {
  1044. return -1;
  1045. }
  1046. }
  1047. else if (a.type == 'kanji') {
  1048. if (b.type == 'radicals') {
  1049. return 1;
  1050. }
  1051. else if (b.type == 'vocabulary') {
  1052. return -1;
  1053. }
  1054. }
  1055. else {
  1056. if (b.type != 'vocabulary') {
  1057. return 1;
  1058. }
  1059. }
  1060. return a.name.localeCompare(b.name);
  1061. });
  1062. saveData();
  1063. updatePage();
  1064. }
  1065. //-------------------------------------------------------------------
  1066. // Mark a single item
  1067. //-------------------------------------------------------------------
  1068. function markItem(listIndex, type, name) {
  1069. loadData();
  1070. var index = indexOf(listIndex, type, name);
  1071. if (index === -1) {
  1072. data.lists[listIndex].items.push({type: type, name: name, radical_character: null, radical_image: null});
  1073. saveData();
  1074. }
  1075. updatePage();
  1076. }
  1077. //-------------------------------------------------------------------
  1078. // Copy all items from the source to the target list
  1079. // aka marks them all in the target list
  1080. //-------------------------------------------------------------------
  1081. function copyAll(sourceListIndex, targetListIndex) {
  1082. loadData();
  1083. pageUpdateIsLocked = true;
  1084. for (var i = 0; i < data.lists[sourceListIndex].items.length; i++) {
  1085. var item = data.lists[sourceListIndex].items[i];
  1086. markItem(targetListIndex, item.type, item.name);
  1087. }
  1088. pageUpdateIsLocked = false;
  1089. updatePage();
  1090. }
  1091.  
  1092. //-------------------------------------------------------------------
  1093. // Callback if the currentItem changes
  1094. //-------------------------------------------------------------------
  1095. function onCurrentItemChanged() {
  1096. updatePage();
  1097. }
  1098. //-------------------------------------------------------------------
  1099. // Extends the review session page
  1100. // - Adds buttons to mark/unmark the current item
  1101. //-------------------------------------------------------------------
  1102. function updateReviewSessionPage() {
  1103. $.jStorage.stopListening('currentItem', onCurrentItemChanged);
  1104. $.jStorage.listenKeyChange('currentItem', onCurrentItemChanged);
  1105. var currentItem = $.jStorage.get('currentItem');
  1106. var name;
  1107. var type;
  1108. if (currentItem.hasOwnProperty('rad')) {
  1109. type = 'radicals';
  1110. name = currentItem.en[0].toLowerCase().replace(/\s/g, "-");
  1111. }
  1112. else if (currentItem.hasOwnProperty('kan')) {
  1113. type = 'kanji';
  1114. name = currentItem.kan;
  1115. }
  1116. else if (currentItem.hasOwnProperty('voc')) {
  1117. type = 'vocabulary';
  1118. name = currentItem.voc;
  1119. }
  1120. else {
  1121. return;
  1122. }
  1123. //console.log('updateReviewSessionPage - type: ' + type);
  1124. //console.log('updateReviewSessionPage - name: ' + name);
  1125. var showReviewWarning = false;
  1126. var addDelayBeforeAnswerIsPossible = false;
  1127. $(data.lists).each(function(listIndex, list) {
  1128. $('#option-mark' + listIndex).remove();
  1129. $('#option-unmark' + listIndex).remove();
  1130. var item_index = indexOf(listIndex, type, name);
  1131. if (item_index === -1) {
  1132. $('#additional-content ul').append('<li id="option-mark' + listIndex + '"><span title="Mark on list ' + list.name + '">Mark ' + list.name + '</span></li>');
  1133. $('#option-mark' + listIndex).on('click', function() { markItem(listIndex, type, name); });
  1134. }
  1135. else {
  1136. $('#additional-content ul').append('<li id="option-unmark' + listIndex + '"><span title="Unmark from list ' + list.name + '">Unmark ' + list.name + '</span></li>');
  1137. $('#option-unmark' + listIndex).on('click', function() { unmarkItem(listIndex, type, name); });
  1138. if (list.settings.showReviewWarning) {
  1139. showReviewWarning = true;
  1140. }
  1141.  
  1142. if (list.settings.addDelayBeforeAnswerIsPossible) {
  1143. addDelayBeforeAnswerIsPossible = true;
  1144. }
  1145. }
  1146. });
  1147. $('#warning1').remove();
  1148. $('#warning2').remove();
  1149. if (showReviewWarning) {
  1150. $('#character').append('<span id="warning1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;!!!</span>');
  1151. $('#character').prepend('<span id="warning2">!!!&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>');
  1152. }
  1153.  
  1154. var currentQuestionType = $('#question-type').attr('class');
  1155. $('#reviewWarning').remove();
  1156. if ((currentItem.id != lastSeenItem) || (currentQuestionType != lastSeenQuestionType)) {
  1157. if (addDelayBeforeAnswerIsPossible) {
  1158. $('#answer-form').prepend('<span id="reviewWarning">&nbsp;<br />Beware, it\'s a marked item! Think for a moment about your answer.<br />&nbsp;</span>');
  1159. $('#answer-form form').hide();
  1160. setTimeout(function updateReviewSessionPage() {
  1161. $('#answer-form form').show();
  1162. $('#answer-form input').focus();
  1163. $('#reviewWarning').remove();
  1164. }, 30000);
  1165. }
  1166. else {
  1167. $('#answer-form form').show();
  1168. $('#answer-form input').focus();
  1169. }
  1170. }
  1171. calculateDynamicWidthForReviewPage();
  1172. lastSeenItem = currentItem.id;
  1173. lastSeenQuestionType = currentQuestionType;
  1174. }
  1175. //-------------------------------------------------------------------
  1176. // Updates the dynamic with for the review page
  1177. //-------------------------------------------------------------------
  1178. function calculateDynamicWidthForReviewPage(){
  1179. var liCount = $('#additional-content ul').children().size();
  1180. var percentage = 100 / liCount;
  1181. percentage -= 0.1;
  1182. var cssDynamicWidth =
  1183. '#additional-content ul li {' +
  1184. ' width: ' + percentage + '% !important' +
  1185. '} ';
  1186.  
  1187. addStyle(cssDynamicWidth);
  1188. }
  1189. //-------------------------------------------------------------------
  1190. // Adds a css to the page
  1191. //-------------------------------------------------------------------
  1192. function addStyle(aCss) {
  1193. var head, style;
  1194. head = document.getElementsByTagName('head')[0];
  1195. if (head) {
  1196. style = document.createElement('style');
  1197. style.setAttribute('type', 'text/css');
  1198. style.textContent = aCss;
  1199. head.appendChild(style);
  1200. return style;
  1201. }
  1202. return null;
  1203. }
  1204. //-------------------------------------------------------------------
  1205. // Extends the review and lesson page
  1206. // - Adds a black/white border around marked/unmarked items displayed on the page
  1207. //-------------------------------------------------------------------
  1208. function updateReviewAndLessonPage() {
  1209. var query = $('div.active li');
  1210. if (query.length == 0) { // Page not completly loaded yet
  1211. setTimeout(updatePage, 1000);
  1212. return;
  1213. }
  1214. updateLinks();
  1215. }
  1216. //-------------------------------------------------------------------
  1217. // Extends the dashboard
  1218. // - Adds a 'marked items' section
  1219. // - Adds common buttons and marked items list to section
  1220. //-------------------------------------------------------------------
  1221. function updateDashboardPage() {
  1222. var query = $('section.progression');
  1223. if (query.length != 1) {
  1224. return;
  1225. }
  1226. $('#marked_items').remove();
  1227. $('section.progression').after('<section id="marked_items" />');
  1228. $(data.lists).each(function(listIndex, list) {
  1229. $('#marked_items').append('<h2>Marked items - List ' + list.name + '</h2>');
  1230. $('#marked_items').append('<p id="marked_items_buttons' + listIndex + '" />');
  1231. addCommonButtons(listIndex);
  1232. addMarkedItemsList(listIndex);
  1233. });
  1234. }
  1235. //-------------------------------------------------------------------
  1236. // Extends the an item page
  1237. // - Adds a 'marked items' section
  1238. // - Adds common buttons and marked items list to section
  1239. // - Adds buttons to mark/unmark the current item
  1240. //-------------------------------------------------------------------
  1241. function updateItemPage(type, name) {
  1242. var query = $('section#information');
  1243. if (query.length != 1) {
  1244. return;
  1245. }
  1246. $('#marked_items').remove();
  1247. $('section#information').after('<section id="marked_items" />');
  1248. $(data.lists).each(function(listIndex, list) {
  1249. $('#marked_items').append('<h2>Marked items - List ' + list.name + '</h2>');
  1250. $('#marked_items').append('<p id="marked_items_buttons' + listIndex + '" />');
  1251.  
  1252. var index = indexOf(listIndex, type, name);
  1253. if (index === -1) {
  1254. var button = $('<button>Mark "' + name + '"</button>');
  1255. button.on('click', function() { markItem(listIndex, type, name); });
  1256. $('#marked_items_buttons' + listIndex).append(button)
  1257. }
  1258. else {
  1259. var button = $('<button>Unmark "' + name + '"</button>');
  1260. button.on('click', function() { unmarkItem(listIndex, type, name); });
  1261. $('#marked_items_buttons' + listIndex).append(button)
  1262. }
  1263.  
  1264. addCommonButtons(listIndex);
  1265. addMarkedItemsList(listIndex);
  1266. });
  1267. }
  1268.  
  1269. //-------------------------------------------------------------------
  1270. // Adds common buttons that are used on multiple locations
  1271. //-------------------------------------------------------------------
  1272. function addCommonButtons(listIndex) {
  1273. if (data.lists[listIndex].items.length > 0) {
  1274. var button = $('<button>Unmark all</button>');
  1275. button.on('click', function() { unmarkAllItems(listIndex); });
  1276. $('#marked_items_buttons' + listIndex).append(button)
  1277. }
  1278. if (data.lists[listIndex].items.length > 0) {
  1279. $(data.lists).each(function(otherListIndex, otherList) {
  1280. if (otherListIndex != listIndex) {
  1281. var button = $('<button>Copy all to List ' + otherList.name + '</button>');
  1282. button.on('click', function() { copyAll(listIndex, otherListIndex); });
  1283. $('#marked_items_buttons' + listIndex).append(button)
  1284. }
  1285. });
  1286. }
  1287. if (data.lists[listIndex].items.length > 0) {
  1288. var button = $('<button>Sort</button>');
  1289. button.on('click', function() { sortItems(listIndex); });
  1290. $('#marked_items_buttons' + listIndex).append(button)
  1291. }
  1292. {
  1293. var button = $('<button>Force refresh</button>');
  1294. button.on('click', function() { forceRefresh(); });
  1295. $('#marked_items_buttons' + listIndex).append(button)
  1296. }
  1297. }
  1298. function buildItemList(listIndex) {
  1299. detect_radical_characters();
  1300. for (var i = 0; i < data.lists[listIndex].items.length; i++) {
  1301. var item = data.lists[listIndex].items[i];
  1302. var typeForClass = item.type;
  1303. if (typeForClass == 'radicals')
  1304. typeForClass = 'radical';
  1305. var itemText = item.name;
  1306. if (item.type == 'radicals') {
  1307. if (item.radical_image && item.radical_image != '') {
  1308. itemText = '<img src="' + item.radical_image + '"/>';
  1309. }
  1310. else if (item.radical_character != '') {
  1311. itemText = item.radical_character;
  1312. }
  1313. else {
  1314. itemText = '<i class="radical-' + item.name + '"></i>';
  1315. }
  1316. }
  1317. $('#marked_items_list' + listIndex).append(
  1318. '<a href="/' + item.type + '/' + item.name + '">' +
  1319. ' <span class="' + typeForClass + '-icon" lang="ja">' +
  1320. ' <span style="display: inline-block; margin-top: 0.1em;" class="japanese-font-styling-correction">' +
  1321. itemText +
  1322. ' </span>' +
  1323. ' </span>' +
  1324. '</a>');
  1325. }
  1326. updateLinks();
  1327. };
  1328. //-------------------------------------------------------------------
  1329. // Adds the list of marked items to the current page
  1330. //-------------------------------------------------------------------
  1331. function addMarkedItemsList(listIndex) {
  1332. $('#marked_items').append('<span id="marked_items_list' + listIndex + '" />')
  1333. if (data.lists[listIndex].items.length == 0) {
  1334. $('#marked_items_list' + listIndex).append('<p>No marked items</p>');
  1335. }
  1336. else {
  1337. WaniKani.query_page_radicals_with_images().then(function() { buildItemList(listIndex); });
  1338. }
  1339. }
  1340. //-------------------------------------------------------------------
  1341. // Uses the radical info to determine if a radical uses a character
  1342. // or an image.
  1343. //-------------------------------------------------------------------
  1344. function detect_radical_characters() {
  1345. var radicals_with_images = WaniKani.get_radicals_with_image();
  1346. $(data.lists).each(function(listIndex, list) {
  1347. $(list.items).each(function(itemIndex, item) {
  1348. if ((item.type == 'radicals') && (item.radical_character == null)) {
  1349. var radical_character = null;
  1350. var foundRadical = radicals_with_images.find(function(radical_with_image) {
  1351. return radical_with_image.name == item.name;
  1352. });
  1353. if (!foundRadical) {
  1354. return;
  1355. }
  1356. console.log('detect_radical_characters: ' + item.name + ' -> ' + foundRadical.character + ' / ' + foundRadical.image);
  1357. list.items[itemIndex].radical_character = foundRadical.character;
  1358. list.items[itemIndex].radical_image = foundRadical.image;
  1359. }
  1360. });
  1361. });
  1362. saveData();
  1363. }
  1364.  
  1365. }());