Wanikani Open Framework

Framework for writing scripts for Wanikani

当前为 2018-07-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Wanikani Open Framework
  3. // @namespace rfindley
  4. // @description Framework for writing scripts for Wanikani
  5. // @version 1.0.36
  6. // @include https://www.wanikani.com/*
  7. // @copyright 2018+, Robin Findley
  8. // @license MIT; http://opensource.org/licenses/MIT
  9. // @run-at document-start
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function(global) {
  14. 'use strict';
  15.  
  16. var version = '1.0.36';
  17. var ignore_missing_indexeddb = false;
  18.  
  19. //########################################################################
  20. //------------------------------
  21. // Supported Modules
  22. //------------------------------
  23. var supported_modules = {
  24. Apiv2: { url: 'https://greasyfork.org/scripts/38581-wanikani-open-framework-apiv2-module/code/Wanikani%20Open%20Framework%20-%20Apiv2%20module.js?version=266930'},
  25. ItemData: { url: 'https://greasyfork.org/scripts/38580-wanikani-open-framework-itemdata-module/code/Wanikani%20Open%20Framework%20-%20ItemData%20module.js?version=611211'},
  26. Menu: { url: 'https://greasyfork.org/scripts/38578-wanikani-open-framework-menu-module/code/Wanikani%20Open%20Framework%20-%20Menu%20module.js?version=612205'},
  27. Progress: { url: 'https://greasyfork.org/scripts/38577-wanikani-open-framework-progress-module/code/Wanikani%20Open%20Framework%20-%20Progress%20module.js?version=601473'},
  28. Settings: { url: 'https://greasyfork.org/scripts/38576-wanikani-open-framework-settings-module/code/Wanikani%20Open%20Framework%20-%20Settings%20module.js?version=607871'},
  29. };
  30.  
  31. //########################################################################
  32. //------------------------------
  33. // Published interface
  34. //------------------------------
  35. var published_interface = {
  36. include: include, // include(module_list) => Promise
  37. ready: ready, // ready(module_list) => Promise
  38.  
  39. load_file: load_file, // load_file(url, use_cache) => Promise
  40. load_css: load_css, // load_css(url, use_cache) => Promise
  41. load_script: load_script, // load_script(url, use_cache) => Promise
  42.  
  43. file_cache: {
  44. dir: {}, // Object containing directory of files.
  45. clear: file_cache_clear, // clear() => Promise
  46. delete: file_cache_delete, // delete(name) => Promise
  47. flush: file_cache_flush, // flush() => Promise
  48. load: file_cache_load, // load(name) => Promise
  49. save: file_cache_save // save(name, content) => Promise
  50. },
  51.  
  52. on: wait_event, // on(event, callback)
  53. trigger: trigger_event, // trigger(event[, data1[, data2[, ...]]])
  54.  
  55. get_state: get_state, // get(state_var)
  56. set_state: set_state, // set(state_var, value)
  57. wait_state: wait_state, // wait(state_var, value[, callback[, persistent]]) => if no callback, return one-shot Promise
  58.  
  59. version: {
  60. value: version,
  61. compare_to: compare_to, // compare_version(version)
  62. }
  63. };
  64.  
  65. published_interface.support_files = {
  66. 'jquery_ui.js': 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js',
  67. 'jqui_wkmain.css': 'https://raw.githubusercontent.com/rfindley/wanikani-open-framework/1550af8383ec28ad406cf401aee2de4c52446f6c/jqui-wkmain.css',
  68. };
  69.  
  70. //########################################################################
  71.  
  72. function split_list(str) {return str.replace(/^\s+|\s*(,)\s*|\s+$/g, '$1').split(',').filter(function(name) {return (name.length > 0);});}
  73. function promise(){var a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}
  74.  
  75. //########################################################################
  76.  
  77. //------------------------------
  78. // Compare the framework version against a specific version.
  79. //------------------------------
  80. function compare_to(client_version) {
  81. var client_ver = client_version.split('.').map(d => Number(d));
  82. var wkof_ver = version.split('.').map(d => Number(d));
  83. var len = Math.max(client_ver.length, wkof_ver.length);
  84. for (var idx = 0; idx < len; idx++) {
  85. var a = client_ver[idx] || 0;
  86. var b = wkof_ver[idx] || 0;
  87. if (a === b) continue;
  88. if (a < b) return 'newer';
  89. return 'older';
  90. }
  91. return 'same';
  92. }
  93.  
  94. //------------------------------
  95. // Include a list of modules.
  96. //------------------------------
  97. var include_promises = {};
  98.  
  99. function include(module_list) {
  100. if (wkof.get_state('wkof.wkof') !== 'ready')
  101. return wkof.ready('wkof').then(function(){return wkof.include(module_list);});
  102. var include_promise = promise();
  103. var module_names = split_list(module_list);
  104. var script_cnt = module_names.length;
  105. if (script_cnt === 0) {
  106. include_promise.resolve({loaded:[], failed:[]});
  107. return include_promise;
  108. }
  109.  
  110. var done_cnt = 0;
  111. var loaded = [], failed = [];
  112. var no_cache = split_list(localStorage.getItem('wkof.include.nocache') || '');
  113. for (var idx = 0; idx < module_names.length; idx++) {
  114. var module_name = module_names[idx];
  115. var module = supported_modules[module_name];
  116. if (!module) {
  117. failed.push({name:module_name, url:undefined});
  118. check_done();
  119. continue;
  120. }
  121. var await_load = include_promises[module_name];
  122. var use_cache = no_cache.indexOf(module_name) < 0;
  123. if (!use_cache) file_cache_delete(module.url);
  124. if (await_load === undefined) include_promises[module_name] = await_load = load_script(module.url, use_cache);
  125. await_load.then(push_loaded, push_failed);
  126. }
  127.  
  128. return include_promise;
  129.  
  130. function push_loaded(url) {
  131. loaded.push(url);
  132. check_done();
  133. }
  134.  
  135. function push_failed(url) {
  136. failed.push(url);
  137. check_done();
  138. }
  139.  
  140. function check_done() {
  141. if (++done_cnt < script_cnt) return;
  142. if (failed.length === 0) include_promise.resolve({loaded:loaded, failed:failed});
  143. else include_promise.reject({error:'Failure loading module', loaded:loaded, failed:failed});
  144. }
  145. }
  146.  
  147. //------------------------------
  148. // Wait for all modules to report that they are ready
  149. //------------------------------
  150. function ready(module_list) {
  151. var module_names = split_list(module_list);
  152.  
  153. var ready_promises = [ ];
  154. for (var idx in module_names) {
  155. var module_name = module_names[idx];
  156. ready_promises.push(wait_state('wkof.' + module_name, 'ready'));
  157. }
  158.  
  159. if (ready_promises.length === 0)
  160. return Promise.resolve();
  161. else if (ready_promises.length === 1)
  162. return ready_promises[0];
  163. else
  164. return Promise.all(ready_promises);
  165. }
  166. //########################################################################
  167.  
  168. //------------------------------
  169. // Load a file asynchronously, and pass the file as resolved Promise data.
  170. //------------------------------
  171. function load_file(url, use_cache) {
  172. var fetch_promise = promise();
  173. var no_cache = split_list(localStorage.getItem('wkof.load_file.nocache') || '');
  174. if (no_cache.indexOf(url) >= 0) use_cache = false;
  175. if (use_cache === true) {
  176. return file_cache_load(url, use_cache).catch(fetch_url);
  177. } else {
  178. return fetch_url();
  179. }
  180.  
  181. // Retrieve file from server
  182. function fetch_url(){
  183. var request = new XMLHttpRequest();
  184. request.onreadystatechange = process_result;
  185. request.open('GET', url, true);
  186. request.send();
  187. return fetch_promise;
  188. }
  189.  
  190. function process_result(event){
  191. if (event.target.readyState !== 4) return;
  192. if (event.target.status >= 400 || event.target.status === 0) return fetch_promise.reject(event.target.status);
  193. if (use_cache) {
  194. file_cache_save(url, event.target.response)
  195. .then(fetch_promise.resolve.bind(null,event.target.response));
  196. } else {
  197. fetch_promise.resolve(event.target.response);
  198. }
  199. }
  200. }
  201.  
  202. //------------------------------
  203. // Load and install a specific file type into the DOM.
  204. //------------------------------
  205. function load_and_append(url, tag_name, location, use_cache) {
  206. url = url.replace(/"/g,'\'');
  207. if (document.querySelector(tag_name+'[uid="'+url+'"]') !== null) return Promise.resolve();
  208. return load_file(url, use_cache).then(append_to_tag);
  209.  
  210. function append_to_tag(content) {
  211. var tag = document.createElement(tag_name);
  212. tag.innerHTML = content;
  213. tag.setAttribute('uid', url);
  214. document.querySelector(location).appendChild(tag);
  215. return url;
  216. }
  217. }
  218.  
  219. //------------------------------
  220. // Load and install a CSS file.
  221. //------------------------------
  222. function load_css(url, use_cache) {
  223. return load_and_append(url, 'style', 'head', use_cache);
  224. }
  225.  
  226. //------------------------------
  227. // Load and install Javascript.
  228. //------------------------------
  229. function load_script(url, use_cache) {
  230. return load_and_append(url, 'script', 'body', use_cache);
  231. }
  232. //########################################################################
  233.  
  234. var state_listeners = {};
  235. var state_values = {};
  236.  
  237. //------------------------------
  238. // Get the value of a state variable, and notify listeners.
  239. //------------------------------
  240. function get_state(state_var) {
  241. return state_values[state_var];
  242. }
  243.  
  244. //------------------------------
  245. // Set the value of a state variable, and notify listeners.
  246. //------------------------------
  247. function set_state(state_var, value) {
  248. var old_value = state_values[state_var];
  249. if (old_value === value) return;
  250. state_values[state_var] = value;
  251.  
  252. // Do listener callbacks, and remove non-persistent listeners
  253. var listeners = state_listeners[state_var];
  254. var persistent_listeners = [ ];
  255. for (var idx in listeners) {
  256. var listener = listeners[idx];
  257. var keep = true;
  258. if (listener.value === value || listener.value === '*') {
  259. keep = listener.persistent;
  260. try {
  261. listener.callback(value, old_value);
  262. } catch (e) {}
  263. }
  264. if (keep) persistent_listeners.push(listener);
  265. }
  266. state_listeners[state_var] = persistent_listeners;
  267. }
  268.  
  269. //------------------------------
  270. // When state of state_var changes to value, call callback.
  271. // If persistent === true, continue listening for additional state changes
  272. // If value is '*', callback will be called for all state changes.
  273. //------------------------------
  274. function wait_state(state_var, value, callback, persistent) {
  275. var promise;
  276. if (callback === undefined) {
  277. promise = new Promise(function(resolve, reject) {
  278. callback = resolve;
  279. });
  280. }
  281. if (state_listeners[state_var] === undefined) state_listeners[state_var] = [ ];
  282. persistent = (persistent === true);
  283. var current_value = state_values[state_var];
  284. if (persistent || value !== current_value) state_listeners[state_var].push({callback:callback, persistent:persistent, value:value});
  285.  
  286. // If it's already at the desired state, call the callback immediately.
  287. if (value === current_value) try {
  288. callback(value, current_value);
  289. } catch (err) {}
  290. return promise;
  291. }
  292. //########################################################################
  293.  
  294. var event_listeners = {};
  295.  
  296. //------------------------------
  297. // Fire an event, which then calls callbacks for any listeners.
  298. //------------------------------
  299. function trigger_event(event) {
  300. var listeners = event_listeners[event];
  301. if (listeners === undefined) return;
  302. var args = [];
  303. Array.prototype.push.apply(args,arguments);
  304. args.shift();
  305. for (var idx in listeners) try {
  306. listeners[idx].apply(null,args);
  307. } catch (err) {}
  308. return global.wkof;
  309. }
  310.  
  311. //------------------------------
  312. // Add a listener for an event.
  313. //------------------------------
  314. function wait_event(event, callback) {
  315. if (event_listeners[event] === undefined) event_listeners[event] = [];
  316. event_listeners[event].push(callback);
  317. return global.wkof;
  318. }
  319. //########################################################################
  320.  
  321. var file_cache_open_promise;
  322.  
  323. //------------------------------
  324. // Open the file_cache database (or return handle if open).
  325. //------------------------------
  326. function file_cache_open() {
  327. if (file_cache_open_promise) return file_cache_open_promise;
  328. var open_promise = promise();
  329. file_cache_open_promise = open_promise;
  330. var request;
  331. request = indexedDB.open('wkof.file_cache');
  332. request.onupgradeneeded = upgrade_db;
  333. request.onsuccess = get_dir;
  334. request.onerror = error;
  335. return open_promise;
  336.  
  337. function error() {
  338. console.log('indexedDB could not open!');
  339. wkof.file_cache.dir = {};
  340. if (ignore_missing_indexeddb)
  341. open_promise.resolve(null);
  342. else
  343. open_promise.reject();
  344. }
  345.  
  346. function upgrade_db(event){
  347. var db = event.target.result;
  348. var store = db.createObjectStore('files', {keyPath:'name'});
  349. }
  350.  
  351. function get_dir(event){
  352. var db = event.target.result;
  353. var transaction = db.transaction('files', 'readonly');
  354. var store = transaction.objectStore('files');
  355. var request = store.get('[dir]');
  356. request.onsuccess = process_dir;
  357. transaction.oncomplete = open_promise.resolve.bind(null, db);
  358. open_promise.then(setTimeout.bind(null, file_cache_cleanup, 10000));
  359. }
  360.  
  361. function process_dir(event){
  362. if (event.target.result === undefined) {
  363. wkof.file_cache.dir = {};
  364. } else {
  365. wkof.file_cache.dir = JSON.parse(event.target.result.content);
  366. }
  367. }
  368. }
  369.  
  370. //------------------------------
  371. // Clear the file_cache database.
  372. //------------------------------
  373. function file_cache_clear() {
  374. return file_cache_open().then(clear);
  375.  
  376. function clear(db) {
  377. var clear_promise = promise();
  378. wkof.file_cache.dir = {};
  379. if (db === null) return clear_promise.resolve();
  380. var transaction = db.transaction('files', 'readwrite');
  381. var store = transaction.objectStore('files');
  382. store.clear();
  383. transaction.oncomplete = clear_promise.resolve;
  384. }
  385. }
  386.  
  387. //------------------------------
  388. // Delete a file from the file_cache database.
  389. //------------------------------
  390. function file_cache_delete(pattern) {
  391. return file_cache_open().then(del);
  392.  
  393. function del(db) {
  394. var del_promise = promise();
  395. if (db === null) return del_promise.resolve();
  396. var transaction = db.transaction('files', 'readwrite');
  397. var store = transaction.objectStore('files');
  398. var files = Object.keys(wkof.file_cache.dir).filter(function(file){
  399. if (pattern instanceof RegExp)
  400. return file.match(pattern) !== null;
  401. else
  402. return (file === pattern);
  403. });
  404. files.forEach(function(file){
  405. store.delete(file);
  406. delete wkof.file_cache.dir[file];
  407. });
  408. file_cache_dir_save();
  409. transaction.oncomplete = del_promise.resolve.bind(null, files);
  410. return del_promise;
  411. }
  412. }
  413.  
  414. //------------------------------
  415. // Force immediate save of file_cache directory.
  416. //------------------------------
  417. function file_cache_flush() {
  418. file_cache_dir_save(true /* immediately */);
  419. }
  420.  
  421. //------------------------------
  422. // Load a file from the file_cache database.
  423. //------------------------------
  424. function file_cache_load(name) {
  425. var load_promise = promise();
  426. return file_cache_open().then(load);
  427.  
  428. function load(db) {
  429. if (wkof.file_cache.dir[name] === undefined) {
  430. load_promise.reject(name);
  431. return load_promise;
  432. }
  433. var transaction = db.transaction('files', 'readonly');
  434. var store = transaction.objectStore('files');
  435. var request = store.get(name);
  436. wkof.file_cache.dir[name].last_loaded = new Date().toISOString();
  437. file_cache_dir_save();
  438. request.onsuccess = finish;
  439. request.onerror = error;
  440. return load_promise;
  441.  
  442. function finish(event){
  443. if (event.target.result === undefined)
  444. load_promise.reject(name);
  445. else
  446. load_promise.resolve(event.target.result.content);
  447. }
  448.  
  449. function error(event){
  450. load_promise.reject(name);
  451. }
  452. }
  453. }
  454.  
  455. //------------------------------
  456. // Save a file into the file_cache database.
  457. //------------------------------
  458. function file_cache_save(name, content, extra_attribs) {
  459. return file_cache_open().then(save);
  460.  
  461. function save(db) {
  462. var save_promise = promise();
  463. if (db === null) return save_promise.resolve(name);
  464. var transaction = db.transaction('files', 'readwrite');
  465. var store = transaction.objectStore('files');
  466. store.put({name:name,content:content});
  467. var now = new Date().toISOString();
  468. wkof.file_cache.dir[name] = Object.assign({added:now, last_loaded:now}, extra_attribs);
  469. file_cache_dir_save(true /* immediately */);
  470. transaction.oncomplete = save_promise.resolve.bind(null, name);
  471. }
  472. }
  473.  
  474. //------------------------------
  475. // Save a the file_cache directory contents.
  476. //------------------------------
  477. var fc_sync_timer;
  478. function file_cache_dir_save(immediately) {
  479. if (fc_sync_timer !== undefined) clearTimeout(fc_sync_timer);
  480. var delay = (immediately ? 0 : 2000);
  481. fc_sync_timer = setTimeout(save, delay);
  482.  
  483. function save(){
  484. file_cache_open().then(save2);
  485. }
  486.  
  487. function save2(db){
  488. fc_sync_timer = undefined;
  489. var transaction = db.transaction('files', 'readwrite');
  490. var store = transaction.objectStore('files');
  491. store.put({name:'[dir]',content:JSON.stringify(wkof.file_cache.dir)});
  492. }
  493. }
  494.  
  495. //------------------------------
  496. // Remove files that haven't been accessed in a while.
  497. //------------------------------
  498. function file_cache_cleanup() {
  499. var threshold = new Date() - 14*86400000; // 14 days
  500. var old_files = [];
  501. for (var fname in wkof.file_cache.dir) {
  502. if (fname.match(/^wkof\.settings\./)) continue; // Don't flush settings files.
  503. var fdate = new Date(wkof.file_cache.dir[fname].last_loaded);
  504. if (fdate < threshold) old_files.push(fname);
  505. }
  506. if (old_files.length === 0) return;
  507. console.log('Cleaning out '+old_files.length+' old file(s) from "wkof.file_cache":');
  508. for (var fnum in old_files) {
  509. console.log(' '+(Number(fnum)+1)+': '+old_files[fnum]);
  510. wkof.file_cache.delete(old_files[fnum]);
  511. }
  512. }
  513.  
  514. function doc_ready() {
  515. wkof.set_state('wkof.document', 'ready');
  516. }
  517.  
  518. //########################################################################
  519. // Bootloader Startup
  520. //------------------------------
  521. function startup() {
  522. global.wkof = published_interface;
  523.  
  524. // Mark document state as 'ready'.
  525. if (document.readyState === 'complete')
  526. doc_ready();
  527. else
  528. window.addEventListener("load", doc_ready, false); // Notify listeners that we are ready.
  529.  
  530. // Open cache, so wkof.file_cache.dir is available to console immediately.
  531. file_cache_open();
  532. wkof.set_state('wkof.wkof', 'ready');
  533. }
  534. startup();
  535.  
  536. })(window);