Wanikani Open Framework

Framework for writing scripts for Wanikani

目前为 2019-05-25 提交的版本。查看 最新版本

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