Wanikani Open Framework

Framework for writing scripts for Wanikani

目前為 2018-04-18 提交的版本,檢視 最新版本

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址