Greasy Fork is available in English.

Userscript App Core

Userscript App Core For Userscript Web Apps

Versione datata 27/09/2022. Vedi la nuova versione l'ultima versione.

  1. /* eslint-disable no-multi-spaces */
  2.  
  3. // ==UserScript==
  4. // @name Userscript App Core
  5. // @name:zh-CN 用户脚本应用核心
  6. // @name:en Userscript App Core
  7. // @namespace Userscript-App
  8. // @version 0.1.2
  9. // @description Userscript App Core For Userscript Web Apps
  10. // @description:zh-CN 用户脚本网页应用核心
  11. // @description:en Userscript App Core For Userscript Web Apps
  12. // @author PY-DNG
  13. // @license GPL-v3
  14. // @match http*://*/*
  15. // @connect *
  16. // @grant GM_info
  17. // @grant GM_addStyle
  18. // @grant GM_addElement
  19. // @grant GM_deleteValue
  20. // @grant GM_listValues
  21. // @grant GM_addValueChangeListener
  22. // @grant GM_removeValueChangeListener
  23. // @grant GM_setValue
  24. // @grant GM_getValue
  25. // @grant GM_log
  26. // @grant GM_getResourceText
  27. // @grant GM_getResourceURL
  28. // @grant GM_registerMenuCommand
  29. // @grant GM_unregisterMenuCommand
  30. // @grant GM_openInTab
  31. // @grant GM_xmlhttpRequest
  32. // @grant GM_download
  33. // @grant GM_getTab
  34. // @grant GM_saveTab
  35. // @grant GM_getTabs
  36. // @grant GM_notification
  37. // @grant GM_setClipboard
  38. // @grant GM_info
  39. // @grant unsafeWindow
  40. // ==/UserScript==
  41.  
  42. (function __MAIN__() {
  43. 'use strict';
  44.  
  45. // Polyfills
  46. const script_name = '新的用户脚本';
  47. const script_version = '0.1';
  48. const NMonkey_Info = {
  49. GM_info: {
  50. script: {
  51. name: script_name,
  52. author: 'PY-DNG',
  53. version: script_version
  54. }
  55. },
  56. mainFunc: __MAIN__
  57. };
  58. const NMonkey_Ready = NMonkey(NMonkey_Info);
  59. if (!NMonkey_Ready) {return false;}
  60. polyfill_replaceAll();
  61.  
  62. // Arguments: level=LogLevel.Info, logContent, asObject=false
  63. // Needs one call "DoLog();" to get it initialized before using it!
  64. function DoLog() {
  65. // Get window
  66. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window ;
  67.  
  68. // Global log levels set
  69. win.LogLevel = {
  70. None: 0,
  71. Error: 1,
  72. Success: 2,
  73. Warning: 3,
  74. Info: 4,
  75. }
  76. win.LogLevelMap = {};
  77. win.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
  78. win.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
  79. win.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
  80. win.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
  81. win.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
  82. win.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
  83.  
  84. // Current log level
  85. DoLog.logLevel = win.isPY_DNG ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  86.  
  87. // Log counter
  88. DoLog.logCount === undefined && (DoLog.logCount = 0);
  89.  
  90. // Get args
  91. let level, logContent, asObject;
  92. switch (arguments.length) {
  93. case 1:
  94. level = LogLevel.Info;
  95. logContent = arguments[0];
  96. asObject = false;
  97. break;
  98. case 2:
  99. level = arguments[0];
  100. logContent = arguments[1];
  101. asObject = false;
  102. break;
  103. case 3:
  104. level = arguments[0];
  105. logContent = arguments[1];
  106. asObject = arguments[2];
  107. break;
  108. default:
  109. level = LogLevel.Info;
  110. logContent = 'DoLog initialized.';
  111. asObject = false;
  112. break;
  113. }
  114.  
  115. // Log when log level permits
  116. if (level <= DoLog.logLevel) {
  117. let msg = '%c' + LogLevelMap[level].prefix;
  118. let subst = LogLevelMap[level].color;
  119.  
  120. if (asObject) {
  121. msg += ' %o';
  122. } else {
  123. switch(typeof(logContent)) {
  124. case 'string': msg += ' %s'; break;
  125. case 'number': msg += ' %d'; break;
  126. case 'object': msg += ' %o'; break;
  127. }
  128. }
  129.  
  130. if (++DoLog.logCount > 512) {
  131. console.clear();
  132. DoLog.logCount = 0;
  133. }
  134. console.log(msg, subst, logContent);
  135. }
  136. }
  137. DoLog();
  138.  
  139. // Constances
  140. const CONST = {Text: {}};
  141.  
  142. // Init language
  143. let i18n = navigator.language;
  144. let i18n_default = 'en';
  145. if (!Object.keys(CONST.Text).includes(i18n)) {i18n = i18n_default;}
  146.  
  147. main();
  148. function main() {
  149. unsafeWindow.GM_grant = GM_grant;
  150. unsafeWindow.dispatchEvent(new Event('gmready'));
  151. }
  152.  
  153. function GM_grant(name) {
  154. const GMFuncs = {
  155. // Tampermonkey provides
  156. GM_addStyle: typeof GM_addStyle === 'function' ? GM_addStyle : null,
  157. GM_addElement: typeof GM_addElement === 'function' ? GM_addElement : null,
  158. GM_deleteValue: typeof GM_deleteValue === 'function' ? GM_deleteValue : null,
  159. GM_listValues: typeof GM_listValues === 'function' ? GM_listValues : null,
  160. GM_addValueChangeListener: typeof GM_addValueChangeListener === 'function' ? GM_addValueChangeListener : null,
  161. GM_removeValueChangeListener: typeof GM_removeValueChangeListener === 'function' ? GM_removeValueChangeListener : null,
  162. GM_setValue: typeof GM_setValue === 'function' ? GM_setValue : null,
  163. GM_getValue: typeof GM_getValue === 'function' ? GM_getValue : null,
  164. GM_log: typeof GM_log === 'function' ? GM_log : null,
  165. GM_getResourceText: typeof GM_getResourceText === 'function' ? GM_getResourceText : null,
  166. GM_getResourceURL: typeof GM_getResourceURL === 'function' ? GM_getResourceURL : null,
  167. GM_registerMenuCommand: typeof GM_registerMenuCommand === 'function' ? GM_registerMenuCommand : null,
  168. GM_unregisterMenuCommand: typeof GM_unregisterMenuCommand === 'function' ? GM_unregisterMenuCommand : null,
  169. GM_openInTab: typeof GM_openInTab === 'function' ? GM_openInTab : null,
  170. GM_xmlhttpRequest: typeof GM_xmlhttpRequest === 'function' ? GM_xmlhttpRequest : null,
  171. GM_download: typeof GM_download === 'function' ? GM_download : null,
  172. GM_getTab: typeof GM_getTab === 'function' ? GM_getTab : null,
  173. GM_saveTab: typeof GM_saveTab === 'function' ? GM_saveTab : null,
  174. GM_getTabs: typeof GM_getTabs === 'function' ? GM_getTabs : null,
  175. GM_notification: typeof GM_notification === 'function' ? GM_notification : null,
  176. GM_setClipboard: typeof GM_setClipboard === 'function' ? GM_setClipboard : null
  177. };
  178. if (Object.keys(GMFuncs).includes(name)) {
  179. return GMFuncs[name];
  180. } else {
  181. return null;
  182. }
  183. }
  184.  
  185. // Basic functions
  186. // querySelector
  187. function $() {
  188. switch(arguments.length) {
  189. case 2:
  190. return arguments[0].querySelector(arguments[1]);
  191. break;
  192. default:
  193. return document.querySelector(arguments[0]);
  194. }
  195. }
  196. // querySelectorAll
  197. function $All() {
  198. switch(arguments.length) {
  199. case 2:
  200. return arguments[0].querySelectorAll(arguments[1]);
  201. break;
  202. default:
  203. return document.querySelectorAll(arguments[0]);
  204. }
  205. }
  206. // createElement
  207. function $CrE() {
  208. switch(arguments.length) {
  209. case 2:
  210. return arguments[0].createElement(arguments[1]);
  211. break;
  212. default:
  213. return document.createElement(arguments[0]);
  214. }
  215. }
  216. // Object1[prop] ==> Object2[prop]
  217. function copyProp(obj1, obj2, prop) {obj1[prop] !== undefined && (obj2[prop] = obj1[prop]);}
  218. function copyProps(obj1, obj2, props) {props.forEach((prop) => (copyProp(obj1, obj2, prop)));}
  219.  
  220. // Just stopPropagation and preventDefault
  221. function destroyEvent(e) {
  222. if (!e) {return false;};
  223. if (!e instanceof Event) {return false;};
  224. e.stopPropagation();
  225. e.preventDefault();
  226. }
  227.  
  228. // GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
  229. // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
  230. // (If the request is invalid, such as url === '', will return false and will NOT make this request)
  231. // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
  232. // Requires: function delItem(){...} & function uniqueIDMaker(){...}
  233. function GMXHRHook(maxXHR=5) {
  234. const GM_XHR = GM_xmlhttpRequest;
  235. const getID = uniqueIDMaker();
  236. let todoList = [], ongoingList = [];
  237. GM_xmlhttpRequest = safeGMxhr;
  238.  
  239. function safeGMxhr() {
  240. // Get an id for this request, arrange a request object for it.
  241. const id = getID();
  242. const request = {id: id, args: arguments, aborter: null};
  243.  
  244. // Deal onload function first
  245. dealEndingEvents(request);
  246.  
  247. /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
  248. // Stop invalid requests
  249. if (!validCheck(request)) {
  250. return false;
  251. }
  252. */
  253.  
  254. // Judge if we could start the request now or later?
  255. todoList.push(request);
  256. checkXHR();
  257. return makeAbortFunc(id);
  258.  
  259. // Decrease activeXHRCount while GM_XHR onload;
  260. function dealEndingEvents(request) {
  261. const e = request.args[0];
  262.  
  263. // onload event
  264. const oriOnload = e.onload;
  265. e.onload = function() {
  266. reqFinish(request.id);
  267. checkXHR();
  268. oriOnload ? oriOnload.apply(null, arguments) : function() {};
  269. }
  270.  
  271. // onerror event
  272. const oriOnerror = e.onerror;
  273. e.onerror = function() {
  274. reqFinish(request.id);
  275. checkXHR();
  276. oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
  277. }
  278.  
  279. // ontimeout event
  280. const oriOntimeout = e.ontimeout;
  281. e.ontimeout = function() {
  282. reqFinish(request.id);
  283. checkXHR();
  284. oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
  285. }
  286.  
  287. // onabort event
  288. const oriOnabort = e.onabort;
  289. e.onabort = function() {
  290. reqFinish(request.id);
  291. checkXHR();
  292. oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
  293. }
  294. }
  295.  
  296. // Check if the request is invalid
  297. function validCheck(request) {
  298. const e = request.args[0];
  299.  
  300. if (!e.url) {
  301. return false;
  302. }
  303.  
  304. return true;
  305. }
  306.  
  307. // Call a XHR from todoList and push the request object to ongoingList if called
  308. function checkXHR() {
  309. if (ongoingList.length >= maxXHR) {return false;};
  310. if (todoList.length === 0) {return false;};
  311. const req = todoList.shift();
  312. const reqArgs = req.args;
  313. const aborter = GM_XHR.apply(null, reqArgs);
  314. req.aborter = aborter;
  315. ongoingList.push(req);
  316. return req;
  317. }
  318.  
  319. // Make a function that aborts a certain request
  320. function makeAbortFunc(id) {
  321. return function() {
  322. let i;
  323.  
  324. // Check if the request haven't been called
  325. for (i = 0; i < todoList.length; i++) {
  326. const req = todoList[i];
  327. if (req.id === id) {
  328. // found this request: haven't been called
  329. delItem(todoList, i);
  330. return true;
  331. }
  332. }
  333.  
  334. // Check if the request is running now
  335. for (i = 0; i < ongoingList.length; i++) {
  336. const req = todoList[i];
  337. if (req.id === id) {
  338. // found this request: running now
  339. req.aborter();
  340. reqFinish(id);
  341. checkXHR();
  342. }
  343. }
  344.  
  345. // Oh no, this request is already finished...
  346. return false;
  347. }
  348. }
  349.  
  350. // Remove a certain request from ongoingList
  351. function reqFinish(id) {
  352. let i;
  353. for (i = 0; i < ongoingList.length; i++) {
  354. const req = ongoingList[i];
  355. if (req.id === id) {
  356. ongoingList = delItem(ongoingList, i);
  357. return true;
  358. }
  359. }
  360. return false;
  361. }
  362. }
  363. }
  364.  
  365. // Get a url argument from lacation.href
  366. // also recieve a function to deal the matched string
  367. // returns defaultValue if name not found
  368. // Args: {url=location.href, name, dealFunc=((a)=>{return a;}), defaultValue=null} or 'name'
  369. function getUrlArgv(details) {
  370. typeof(details) === 'string' && (details = {name: details});
  371. typeof(details) === 'undefined' && (details = {});
  372. if (!details.name) {return null;};
  373.  
  374. const url = details.url ? details.url : location.href;
  375. const name = details.name ? details.name : '';
  376. const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
  377. const defaultValue = details.defaultValue ? details.defaultValue : null;
  378. const matcher = new RegExp('[\\?&]' + name + '=([^&#]+)');
  379. const result = url.match(matcher);
  380. const argv = result ? dealFunc(result[1]) : defaultValue;
  381.  
  382. return argv;
  383. }
  384.  
  385. // Append a style text to document(<head>) with a <style> element
  386. function addStyle(css, id) {
  387. const style = document.createElement("style");
  388. id && (style.id = id);
  389. style.textContent = css;
  390. for (const elm of document.querySelectorAll('#'+id)) {
  391. elm.parentElement && elm.parentElement.removeChild(elm);
  392. }
  393. document.head.appendChild(style);
  394. }
  395.  
  396. // Save dataURL to file
  397. function saveFile(dataURL, filename) {
  398. const a = document.createElement('a');
  399. a.href = dataURL;
  400. a.download = filename;
  401. a.click();
  402. }
  403.  
  404. // File download function
  405. // details looks like the detail of GM_xmlhttpRequest
  406. // onload function will be called after file saved to disk
  407. function downloadFile(details) {
  408. if (!details.url || !details.name) {return false;};
  409.  
  410. // Configure request object
  411. const requestObj = {
  412. url: details.url,
  413. responseType: 'blob',
  414. onload: function(e) {
  415. // Save file
  416. saveFile(URL.createObjectURL(e.response), details.name);
  417.  
  418. // onload callback
  419. details.onload ? details.onload(e) : function() {};
  420. }
  421. }
  422. if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
  423. if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
  424. if (details.onerror ) {requestObj.onerror = details.onerror;};
  425. if (details.onabort ) {requestObj.onabort = details.onabort;};
  426. if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
  427. if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
  428.  
  429. // Send request
  430. GM_xmlhttpRequest(requestObj);
  431. }
  432.  
  433. // get '/' splited API array from a url
  434. function getAPI(url=location.href) {
  435. return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
  436. }
  437.  
  438. // get host part from a url(includes '^https://', '/$')
  439. function getHost(url=location.href) {
  440. const match = location.href.match(/https?:\/\/[^\/]+\//);
  441. return match ? match[0] : match;
  442. }
  443.  
  444. function AsyncManager() {
  445. const AM = this;
  446.  
  447. // Ongoing xhr count
  448. this.taskCount = 0;
  449.  
  450. // Whether generate finish events
  451. let finishEvent = false;
  452. Object.defineProperty(this, 'finishEvent', {
  453. configurable: true,
  454. enumerable: true,
  455. get: () => (finishEvent),
  456. set: (b) => {
  457. finishEvent = b;
  458. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  459. }
  460. });
  461.  
  462. // Add one task
  463. this.add = () => (++AM.taskCount);
  464.  
  465. // Finish one task
  466. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  467. }
  468.  
  469. // NMonkey By PY-DNG, 2021.07.18 - 2022.02.18, License GPL-3
  470. // NMonkey: Provides GM_Polyfills and make your userscript compatible with non-script-manager environment
  471. // Description:
  472. /*
  473. Simulates a script-manager environment("NMonkey Environment") for non-script-manager browser, load @require & @resource, provides some GM_functions(listed below), and also compatible with script-manager environment.
  474. Provides GM_setValue, GM_getValue, GM_deleteValue, GM_listValues, GM_xmlhttpRequest, GM_openInTab, GM_setClipboard, GM_getResourceText, GM_getResourceURL, GM_addStyle, GM_addElement, GM_log, unsafeWindow(object), GM_info(object)
  475. Also provides an object called GM_POLYFILLED which has the following properties that shows you which GM_functions are actually polyfilled.
  476. Returns true if polyfilled is environment is ready, false for not. Don't worry, just follow the usage below.
  477. */
  478. // Note: DO NOT DEFINE GM-FUNCTION-NAMES IN YOUR CODE. DO NOT DEFINE GM_POLYFILLED AS WELL.
  479. // Note: NMonkey is an advanced version of GM_PolyFill (and BypassXB), it includes more functions than GM_PolyFill, and provides better stability and compatibility. Do NOT use NMonkey and GM_PolyFill (and BypassXB) together in one script.
  480. // Usage:
  481. /*
  482. // ==UserScript==
  483. // @name xxx
  484. // @namespace xxx
  485. // @version 1.0
  486. // ...
  487. // @require https://.../xxx.js
  488. // @require ...
  489. // ...
  490. // @resource https://.../xxx
  491. // @resource ...
  492. // ...
  493. // ==/UserScript==
  494.  
  495. // Use a closure to wrap your code. Make sure you have it a name.
  496. (function YOUR_MAIN_FUNCTION() {
  497. 'use strict';
  498. // Strict mode is optional. You can use strict mode or not as you want.
  499. // Polyfill first. Do NOT do anything before Polyfill.
  500. var NMonkey_Ready = NMonkey({
  501. mainFunc: YOUR_MAIN_FUNCTION,
  502. name: "script-storage-key, aims to separate different scripts' storage area. Use your script's @namespace value if you don't how to fill this field.",
  503. requires: [
  504. {
  505. name: "", // Optional, used to display loading error messages if anything went wrong while loading this item
  506. src: "https://.../xxx.js",
  507. loaded: function() {return boolean_value_shows_whether_this_js_has_already_loaded;}
  508. execmode: "'eval' for eval code in current scope or 'function' for Function(code)() in global scope or 'script' for inserting a <script> element to document.head"
  509. },
  510. ...
  511. ],
  512. resources: [
  513. {
  514. src: "https://.../xxx"
  515. name: "@resource name. Will try to get it from @resource using this name before fetch it from src",
  516. },
  517. ...
  518. ],
  519. GM_info: {
  520. // You can get GM_info object, if you provide this argument(and there is no GM_info provided by the script-manager).
  521. // You can provide any object here, what you provide will be what you get.
  522. // Additionally, two property of NMonkey itself will be attached to GM_info if polyfilled:
  523. // {
  524. // scriptHandler: "NMonkey"
  525. // version: "NMonkey's version, it should look like '0.1'"
  526. // }
  527. // The following is just an example.
  528. script: {
  529. name: 'my first userscript for non-scriptmanager browsers!',
  530. description: 'this script works well both in my PC and my mobile!',
  531. version: '1.0',
  532. released: true,
  533. version_num: 1,
  534. authors: ['Johnson', 'Leecy', 'War Mars']
  535. update_history: {
  536. '0.9': 'First beta version',
  537. '1.0': 'Finally released!'
  538. }
  539. }
  540. surprise: 'if you check GM_info.surprise and you will read this!'
  541. // And property "scriptHandler" & "version" will be attached here
  542. }
  543. });
  544. if (!NMonkey_Ready) {
  545. // Stop executing of polyfilled environment not ready.
  546. // Don't worry, during polyfill progress YOUR_MAIN_FUNCTION will be called twice, and on the second call the polyfilled environment will be ready.
  547. return;
  548. }
  549.  
  550. // Your code here...
  551. // Make sure your code is written after NMonkey be called
  552. if
  553. // ...
  554.  
  555. // Just place NMonkey function code here
  556. function NMonkey(details) {
  557. ...
  558. }
  559. }) ();
  560.  
  561. // Oh you want to write something here? Fine. But code you write here cannot get into the simulated script-manager-environment.
  562. */
  563. function NMonkey(details) {
  564. // Constances
  565. const CONST = {
  566. Text: {
  567. Require_Load_Failed: '动态加载依赖js库失败(自动重试也都失败了),请刷新页面后再试:(\n一共尝试了{I}个备用加载源\n加载项目:{N}',
  568. Resource_Load_Failed: '动态加载依赖resource资源失败(自动重试也都失败了),请刷新页面后再试:(\n一共尝试了{I}个备用加载源\n加载项目:{N}',
  569. UnkownItem: '未知项目',
  570. }
  571. };
  572.  
  573. // Init DoLog
  574. DoLog();
  575.  
  576. // Get argument
  577. const mainFunc = details.mainFunc;
  578. const name = details.name || 'default';
  579. const requires = details.requires || [];
  580. const resources = details.resources || [];
  581. details.GM_info = details.GM_info || {};
  582. details.GM_info.scriptHandler = 'NMonkey';
  583. details.GM_info.version = '1.0';
  584.  
  585. // Run in variable-name-polifilled environment
  586. if (InNPEnvironment()) {
  587. // Already in polifilled environment === polyfill has alredy done, just return
  588. return true;
  589. }
  590.  
  591. // Polyfill functions and data
  592. const GM_POLYFILL_KEY_STORAGE = 'GM_STORAGE_POLYFILL';
  593. let GM_POLYFILL_storage;
  594. const Supports = {
  595. GetStorage: function() {
  596. let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
  597. gstorage = gstorage ? JSON.parse(gstorage) : {};
  598. let storage = gstorage[name] ? gstorage[name] : {};
  599. return storage;
  600. },
  601.  
  602. SaveStorage: function() {
  603. let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
  604. gstorage = gstorage ? JSON.parse(gstorage) : {};
  605. gstorage[name] = GM_POLYFILL_storage;
  606. localStorage.setItem(GM_POLYFILL_KEY_STORAGE, JSON.stringify(gstorage));
  607. },
  608. };
  609. const Provides = {
  610. // GM_setValue
  611. GM_setValue: function(name, value) {
  612. GM_POLYFILL_storage = Supports.GetStorage();
  613. name = String(name);
  614. GM_POLYFILL_storage[name] = value;
  615. Supports.SaveStorage();
  616. },
  617.  
  618. // GM_getValue
  619. GM_getValue: function(name, defaultValue) {
  620. GM_POLYFILL_storage = Supports.GetStorage();
  621. name = String(name);
  622. if (GM_POLYFILL_storage.hasOwnProperty(name)) {
  623. return GM_POLYFILL_storage[name];
  624. } else {
  625. return defaultValue;
  626. }
  627. },
  628.  
  629. // GM_deleteValue
  630. GM_deleteValue: function(name) {
  631. GM_POLYFILL_storage = Supports.GetStorage();
  632. name = String(name);
  633. if (GM_POLYFILL_storage.hasOwnProperty(name)) {
  634. delete GM_POLYFILL_storage[name];
  635. Supports.SaveStorage();
  636. }
  637. },
  638.  
  639. // GM_listValues
  640. GM_listValues: function() {
  641. GM_POLYFILL_storage = Supports.GetStorage();
  642. return Object.keys(GM_POLYFILL_storage);
  643. },
  644.  
  645. // unsafeWindow
  646. unsafeWindow: window,
  647.  
  648. // GM_xmlhttpRequest
  649. // not supported properties of details: synchronous binary nocache revalidate context fetch
  650. // not supported properties of response(onload arguments[0]): finalUrl
  651. // ---!IMPORTANT!--- DOES NOT SUPPORT CROSS-ORIGIN REQUESTS!!!!! ---!IMPORTANT!---
  652. // details.synchronous is not supported as Tampermonkey
  653. GM_xmlhttpRequest: function(details) {
  654. const xhr = new XMLHttpRequest();
  655.  
  656. // open request
  657. const openArgs = [details.method, details.url, true];
  658. if (details.user && details.password) {
  659. openArgs.push(details.user);
  660. openArgs.push(details.password);
  661. }
  662. xhr.open.apply(xhr, openArgs);
  663.  
  664. // set headers
  665. if (details.headers) {
  666. for (const key of Object.keys(details.headers)) {
  667. xhr.setRequestHeader(key, details.headers[key]);
  668. }
  669. }
  670. details.cookie ? xhr.setRequestHeader('cookie', details.cookie) : function () {};
  671. details.anonymous ? xhr.setRequestHeader('cookie', '') : function () {};
  672.  
  673. // properties
  674. xhr.timeout = details.timeout;
  675. xhr.responseType = details.responseType;
  676. details.overrideMimeType ? xhr.overrideMimeType(details.overrideMimeType) : function () {};
  677.  
  678. // events
  679. xhr.onabort = details.onabort;
  680. xhr.onerror = details.onerror;
  681. xhr.onloadstart = details.onloadstart;
  682. xhr.onprogress = details.onprogress;
  683. xhr.onreadystatechange = details.onreadystatechange;
  684. xhr.ontimeout = details.ontimeout;
  685. xhr.onload = function (e) {
  686. const response = {
  687. readyState: xhr.readyState,
  688. status: xhr.status,
  689. statusText: xhr.statusText,
  690. responseHeaders: xhr.getAllResponseHeaders(),
  691. response: xhr.response
  692. };
  693. (details.responseType === '' || details.responseType === 'text') ? (response.responseText = xhr.responseText) : function () {};
  694. (details.responseType === '' || details.responseType === 'document') ? (response.responseXML = xhr.responseXML) : function () {};
  695. details.onload(response);
  696. }
  697.  
  698. // send request
  699. details.data ? xhr.send(details.data) : xhr.send();
  700.  
  701. return {
  702. abort: xhr.abort
  703. };
  704. },
  705.  
  706. // NOTE: options(arg2) is NOT SUPPORTED! if provided, then will just be skipped.
  707. GM_openInTab: function(url) {
  708. window.open(url);
  709. },
  710.  
  711. // NOTE: needs to be called in an event handler function, and info(arg2) is NOT SUPPORTED!
  712. GM_setClipboard: function(text) {
  713. // Create a new textarea for copying
  714. const newInput = document.createElement('textarea');
  715. document.body.appendChild(newInput);
  716. newInput.value = text;
  717. newInput.select();
  718. document.execCommand('copy');
  719. document.body.removeChild(newInput);
  720. },
  721.  
  722. GM_getResourceText: function(name) {
  723. const _get = typeof(GM_getResourceText) === 'function' ? GM_getResourceText : () => (null);
  724. let text = _get(name);
  725. if (text) {return text;}
  726. for (const resource of resources) {
  727. if (resource.name === name) {
  728. return resource.content ? resource.content : null;
  729. }
  730. }
  731. return null;
  732. },
  733.  
  734. GM_getResourceURL: function(name) {
  735. const _get = typeof(GM_getResourceURL) === 'function' ? GM_getResourceURL : () => (null);
  736. let url = _get(name);
  737. if (url) {return url;}
  738. for (const resource of resources) {
  739. if (resource.name === name) {
  740. return resource.src ? btoa(resource.src) : null;
  741. }
  742. }
  743. return null;
  744. },
  745.  
  746. GM_addStyle: function(css) {
  747. const style = document.createElement('style');
  748. style.innerHTML = css;
  749. document.head.appendChild(style);
  750. },
  751.  
  752. GM_addElement: function() {
  753. let parent_node, tag_name, attributes;
  754. const head_elements = ['title', 'base', 'link', 'style', 'meta', 'script', 'noscript'/*, 'template'*/];
  755. if (arguments.length === 2) {
  756. tag_name = arguments[0];
  757. attributes = arguments[1];
  758. parent_node = head_elements.includes(tag_name.toLowerCase()) ? document.head : document.body;
  759. } else if (arguments.length === 3) {
  760. parent_node = arguments[0];
  761. tag_name = arguments[1];
  762. attributes = arguments[2];
  763. }
  764. const element = document.createElement(tag_name);
  765. for (const [prop, value] of Object.entries(attributes)) {
  766. element[prop] = value;
  767. }
  768. parent_node.appendChild(element);
  769. },
  770.  
  771. GM_log: function() {
  772. const args = [];
  773. for (let i = 0; i < arguments.length; i++) {
  774. args[i] = arguments[i];
  775. }
  776. console.log.apply(null, args);
  777. },
  778.  
  779. GM_info: details.GM_info,
  780.  
  781. GM: {info: details.GM_info}
  782. };
  783. const _GM_POLYFILLED = Provides.GM_POLYFILLED = {};
  784. for (const pname of Object.keys(Provides)) {
  785. _GM_POLYFILLED[pname] = true;
  786. }
  787.  
  788. // Not in polifilled environment, then polyfill functions and create & move into the environment
  789. // Bypass xbrowser's useless GM_functions
  790. bypassXB();
  791.  
  792. // Create & move into polifilled environment
  793. ExecInNPEnv();
  794.  
  795. return false;
  796.  
  797. // Bypass xbrowser's useless GM_functions
  798. function bypassXB() {
  799. if (typeof(mbrowser) === 'object' || (typeof(GM_info) === 'object' && GM_info.scriptHandler === 'XMonkey')) {
  800. // Useless functions in XMonkey 1.0
  801. const GM_funcs = [
  802. 'unsafeWindow',
  803. 'GM_getValue',
  804. 'GM_setValue',
  805. 'GM_listValues',
  806. 'GM_deleteValue',
  807. //'GM_xmlhttpRequest',
  808. ];
  809. for (const GM_func of GM_funcs) {
  810. window[GM_func] = undefined;
  811. eval('typeof({F}) === "function" && ({F} = Provides.{F});'.replaceAll('{F}', GM_func));
  812. }
  813. // Delete dirty data saved by these stupid functions before
  814. for (let i = 0; i < localStorage.length; i++) {
  815. const key = localStorage.key(i);
  816. const value = localStorage.getItem(key);
  817. value === '[object Object]' && localStorage.removeItem(key);
  818. }
  819. }
  820. }
  821.  
  822. // Check if already in name-predefined environment
  823. // I think there won't be anyone else wants to use this fxxking variable name...
  824. function InNPEnvironment() {
  825. return (typeof(GM_POLYFILLED) === 'object' && GM_POLYFILLED !== null && GM_POLYFILLED !== window.GM_POLYFILLED) ? true : false;
  826. }
  827.  
  828. function ExecInNPEnv() {
  829. const NG = new NameGenerator();
  830.  
  831. // Init names
  832. const tnames = ['context', 'fapply', 'CDATA', 'uneval', 'define', 'module', 'exports', 'window', 'globalThis', 'console', 'cloneInto', 'exportFunction', 'createObjectIn', 'GM', 'GM_info'];
  833. const pnames = Object.keys(Provides);
  834. const fnames = tnames.slice();
  835. const argvlist = [];
  836. const argvs = [];
  837.  
  838. // Add provides
  839. for (const pname of pnames) {
  840. !fnames.includes(pname) && fnames.push(pname);
  841. }
  842.  
  843. // Add grants
  844. if (typeof(GM_info) === 'object' && GM_info.script && GM_info.script.grant) {
  845. for (const gname of GM_info.script.grant) {
  846. !fnames.includes(gname) && fnames.push(gname);
  847. }
  848. }
  849.  
  850. // Make name code
  851. for (let i = 0; i < fnames.length; i++) {
  852. const fname = fnames[i];
  853. const exist = eval('typeof ' + fname + ' !== "undefined"') && fname !== 'GM_POLYFILLED';
  854. argvlist[i] = exist ? fname : (Provides.hasOwnProperty(fname) ? 'Provides.'+fname : '');
  855. argvs[i] = exist ? eval(fname) : (Provides.hasOwnProperty(fname) ? Provides[name] : undefined);
  856. pnames.includes(fname) && (_GM_POLYFILLED[fname] = !exist);
  857. }
  858.  
  859. // Load all @require and @resource
  860. loadRequires(requires, resources, function(requires, resources) {
  861. // Join requirecode
  862. let requirecode = '';
  863. for (const require of requires) {
  864. const mode = require.execmode ? require.execmode : 'eval';
  865. const content = require.content;
  866. if (!content) {continue;}
  867. switch(mode) {
  868. case 'eval':
  869. requirecode += content + '\n';
  870. break;
  871. case 'function': {
  872. const func = Function.apply(null, fnames.concat(content));
  873. func.apply(null, argvs);
  874. break;
  875. }
  876. case 'script': {
  877. const s = document.createElement('script');
  878. s.innerHTML = content;
  879. document.head.appendChild(s);
  880. break;
  881. }
  882. }
  883. }
  884.  
  885. // Make final code & eval
  886. const varnames = ['NG', 'tnames', 'pnames', 'fnames', 'argvist', 'argvs', 'code', 'finalcode', 'wrapper', 'ExecInNPEnv', 'GM_POLYFILL_KEY_STORAGE', 'GM_POLYFILL_storage', 'InNPEnvironment', 'NameGenerator', 'LocalCDN', 'loadRequires', 'requestText', 'Provides', 'Supports', 'bypassXB', 'details', 'mainFunc', 'name', 'requires', 'resources', '_GM_POLYFILLED', 'CONST', 'NMonkey', 'polyfill_status'];
  887. const code = requirecode + 'let ' + varnames.join(', ') + ';\n(' + mainFunc.toString() + ') ();';
  888. const wrapper = Function.apply(null, fnames.concat(code));
  889. const finalcode = '(' + wrapper.toString() + ').apply(this, [' + argvlist.join(', ') + ']);';
  890. eval(finalcode);
  891. });
  892.  
  893. function NameGenerator() {
  894. const NG = this;
  895. const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  896. let index = [0];
  897.  
  898. NG.generate = function() {
  899. const chars = [];
  900. indexIncrease();
  901. for (let i = 0; i < index.length; i++) {
  902. chars[i] = letters.charAt(index[i]);
  903. }
  904. return chars.join('');
  905. }
  906.  
  907. NG.randtext = function(len=32) {
  908. const chars = [];
  909. for (let i = 0; i < len; i++) {
  910. chars[i] = letters[randint(0, letter.length-1)];
  911. }
  912. return chars.join('');
  913. }
  914.  
  915. function indexIncrease(i=0) {
  916. index[i] === undefined && (index[i] = -1);
  917. ++index[i] >= letters.length && (index[i] = 0, indexIncrease(i+1));
  918. }
  919.  
  920. function randint(min, max) {
  921. return Math.floor(Math.random() * (max - min + 1)) + min;
  922. }
  923. }
  924. }
  925.  
  926. // Load all @require and @resource for non-GM/TM environments (such as Alook javascript extension)
  927. // Requirements: function AsyncManager(){...}, function LocalCDN(){...}
  928. function loadRequires(requires, resoures, callback, args=[]) {
  929. // LocalCDN
  930. const LCDN = new LocalCDN();
  931.  
  932. // AsyncManager
  933. const AM = new AsyncManager();
  934. AM.onfinish = function() {
  935. callback.apply(null, [requires, resoures].concat(args));
  936. }
  937.  
  938. // Load js
  939. for (const js of requires) {
  940. !js.loaded() && loadinJs(js);
  941. }
  942.  
  943. // Load resource
  944. for (const resource of resoures) {
  945. loadinResource(resource);
  946. }
  947.  
  948. AM.finishEvent = true;
  949.  
  950. function loadinJs(js) {
  951. AM.add();
  952.  
  953. const srclist = js.srcset ? LCDN.sort(js.srcset).srclist : [];
  954. let i = -1;
  955. LCDN.get(js.src, onload, [], onfail);
  956.  
  957. function onload(content) {
  958. js.content = content;
  959. AM.finish();
  960. }
  961.  
  962. function onfail() {
  963. i++;
  964. if (i < srclist.length) {
  965. LCDN.get(srclist[i], onload, [], onfail);
  966. } else {
  967. alert(CONST.Text.Require_Load_Failed.replace('{I}', i.toString()).replace('{N}', js.name ? js.name : CONST.Text.UnkownItem));
  968. }
  969. }
  970. }
  971.  
  972. function loadinResource(resource) {
  973. let content;
  974. if (typeof GM_getResourceText === 'function' && (content = GM_getResourceText(resource.name))) {
  975. resource.content = content;
  976. } else {
  977. AM.add();
  978.  
  979. let i = -1;
  980. LCDN.get(resource.src, onload, [], onfail);
  981.  
  982. function onload(content) {
  983. resource.content = content;
  984. AM.finish();
  985. }
  986.  
  987. function onfail(content) {
  988. i++;
  989. if (resource.srcset && i < resource.srcset.length) {
  990. LCDN.get(resource.srcset[i], onload, [], onfail);
  991. } else {
  992. debugger;
  993. alert(CONST.Text.Resource_Load_Failed.replace('{I}', i.toString()).replace('{N}', js.name ? js.name : CONST.Text.UnkownItem));
  994. }
  995. }
  996. }
  997. }
  998. }
  999.  
  1000. // Loads web resources and saves them to GM-storage
  1001. // Tries to load web resources from GM-storage in subsequent calls
  1002. // Updates resources every $(this.expire) hours, or use $(this.refresh) function to update all resources instantly
  1003. // Dependencies: GM_getValue(), GM_setValue(), requestText(), AsyncManager(), KEY_LOCALCDN
  1004. function LocalCDN() {
  1005. const LC = this;
  1006. const _GM_getValue = typeof(GM_getValue) === 'function' ? GM_getValue : Provides.GM_getValue;
  1007. const _GM_setValue = typeof(GM_setValue) === 'function' ? GM_setValue : Provides.GM_setValue;
  1008.  
  1009. const KEY_LOCALCDN = 'LOCAL-CDN';
  1010. const KEY_LOCALCDN_VERSION = 'version';
  1011. const VALUE_LOCALCDN_VERSION = '0.3';
  1012.  
  1013. // Default expire time (by hour)
  1014. LC.expire = 72;
  1015.  
  1016. // Try to get resource content from loaclCDN first, if failed/timeout, request from web && save to LocalCDN
  1017. // Accepts callback only: onload & onfail(optional)
  1018. // Returns true if got from LocalCDN, false if got from web
  1019. LC.get = function(url, onload, args=[], onfail=function(){}) {
  1020. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  1021. const resource = CDN[url];
  1022. const time = (new Date()).getTime();
  1023.  
  1024. if (resource && resource.content !== null && !expired(time, resource.time)) {
  1025. onload.apply(null, [resource.content].concat(args));
  1026. return true;
  1027. } else {
  1028. LC.request(url, _onload, [], onfail);
  1029. return false;
  1030. }
  1031.  
  1032. function _onload(content) {
  1033. onload.apply(null, [content].concat(args));
  1034. }
  1035. }
  1036.  
  1037. // Generate resource obj and set to CDN[url]
  1038. // Returns resource obj
  1039. // Provide content means load success, provide null as content means load failed
  1040. LC.set = function(url, content) {
  1041. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  1042. const time = (new Date()).getTime();
  1043. const resource = {
  1044. url: url,
  1045. time: time,
  1046. content: content,
  1047. success: content !== null ? (CDN[url] ? CDN[url].success + 1 : 1) : (CDN[url] ? CDN[url].success : 0),
  1048. fail: content === null ? (CDN[url] ? CDN[url].fail + 1 : 1) : (CDN[url] ? CDN[url].fail : 0),
  1049. };
  1050. CDN[url] = resource;
  1051. _GM_setValue(KEY_LOCALCDN, CDN);
  1052. return resource;
  1053. }
  1054.  
  1055. // Delete one resource from LocalCDN
  1056. LC.delete = function(url) {
  1057. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  1058. if (!CDN[url]) {
  1059. return false;
  1060. } else {
  1061. delete CDN[url];
  1062. _GM_setValue(KEY_LOCALCDN, CDN);
  1063. return true;
  1064. }
  1065. }
  1066.  
  1067. // Delete all resources in LocalCDN
  1068. LC.clear = function() {
  1069. _GM_setValue(KEY_LOCALCDN, {});
  1070. upgradeConfig();
  1071. }
  1072.  
  1073. // List all resource saved in LocalCDN
  1074. LC.list = function() {
  1075. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  1076. const urls = LC.listurls();
  1077. return LC.listurls().map((url) => (CDN[url]));
  1078. }
  1079.  
  1080. // List all resource's url saved in LocalCDN
  1081. LC.listurls = function() {
  1082. return Object.keys(_GM_getValue(KEY_LOCALCDN, {})).filter((url) => (url !== KEY_LOCALCDN_VERSION));
  1083. }
  1084.  
  1085. // Request content from web and save it to CDN[url]
  1086. // Accepts callbacks only: onload & onfail(optional)
  1087. LC.request = function(url, onload, args=[], onfail=function(){}) {
  1088. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  1089. requestText(url, _onload, [], _onfail);
  1090.  
  1091. function _onload(content) {
  1092. LC.set(url, content);
  1093. onload.apply(null, [content].concat(args));
  1094. }
  1095.  
  1096. function _onfail() {
  1097. LC.set(url, null);
  1098. onfail();
  1099. }
  1100. }
  1101.  
  1102. // Re-request all resources in CDN instantly, ignoring LC.expire
  1103. LC.refresh = function(callback, args=[]) {
  1104. const urls = LC.listurls();
  1105.  
  1106. const AM = new AsyncManager();
  1107. AM.onfinish = function() {
  1108. callback.apply(null, [].concat(args))
  1109. };
  1110.  
  1111. for (const url of urls) {
  1112. AM.add();
  1113. LC.request(url, function() {
  1114. AM.finish();
  1115. });
  1116. }
  1117.  
  1118. AM.finishEvent = true;
  1119. }
  1120.  
  1121. // Sort src && srcset, to get a best request sorting
  1122. LC.sort = function(srcset) {
  1123. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  1124. const result = {srclist: [], lists: []};
  1125. const lists = result.lists;
  1126. const srclist = result.srclist;
  1127. const suc_rec = lists[0] = []; // Recent successes take second (not expired yet)
  1128. const suc_old = lists[1] = []; // Old successes take third
  1129. const fails = lists[2] = []; // Fails & unused take the last place
  1130. const time = (new Date()).getTime();
  1131.  
  1132. // Make lists
  1133. for (const s of srcset) {
  1134. const resource = CDN[s];
  1135. if (resource && resource.content !== null) {
  1136. if (!expired(resource.time, time)) {
  1137. suc_rec.push(s);
  1138. } else {
  1139. suc_old.push(s);
  1140. }
  1141. } else {
  1142. fails.push(s);
  1143. }
  1144. }
  1145.  
  1146. // Sort lists
  1147. // Recently successed: Choose most recent ones
  1148. suc_rec.sort((res1, res2) => (res2.time - res1.time));
  1149. // Successed long ago or failed: Sort by success rate & tried time
  1150. [suc_old, fails].forEach((arr) => (arr.sort(sorting)));
  1151.  
  1152. // Push all resources into seclist
  1153. [suc_rec, suc_old, fails].forEach((arr) => (arr.forEach((res) => (srclist.push(res)))));
  1154.  
  1155. DoLog(['LocalCDN: sorted', result]);
  1156. return result;
  1157.  
  1158. function sorting(res1, res2) {
  1159. const sucRate1 = (res1.success+1) / (res1.fail+1);
  1160. const sucRate2 = (res2.success+1) / (res2.fail+1);
  1161.  
  1162. if (sucRate1 !== sucRate2) {
  1163. // Success rate: high to low
  1164. return sucRate2 - sucRate1;
  1165. } else {
  1166. // Tried time: less to more
  1167. // Less tried time means newer added source
  1168. return (res1.success+res1.fail) - (res2.success+res2.fail);
  1169. }
  1170. }
  1171. }
  1172.  
  1173. function upgradeConfig() {
  1174. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  1175. switch(CDN[KEY_LOCALCDN_VERSION]) {
  1176. case undefined:
  1177. init();
  1178. break;
  1179. case '0.1':
  1180. v01_To_v02();
  1181. logUpgrade();
  1182. break;
  1183. case '0.2':
  1184. v01_To_v02();
  1185. v02_To_v03();
  1186. logUpgrade();
  1187. break;
  1188. case VALUE_LOCALCDN_VERSION:
  1189. DoLog('LocalCDN is in latest version.');
  1190. break;
  1191. default:
  1192. DoLog(LogLevel.Error, 'LocalCDN.upgradeConfig: Invalid config version({V}) for LocalCDN. '.replace('{V}', CDN[KEY_LOCALCDN_VERSION]));
  1193. }
  1194. CDN[KEY_LOCALCDN_VERSION] = VALUE_LOCALCDN_VERSION;
  1195. _GM_setValue(KEY_LOCALCDN, CDN);
  1196.  
  1197. function logUpgrade() {
  1198. DoLog(LogLevel.Success, 'LocalCDN successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', CDN[KEY_LOCALCDN_VERSION]).replaceAll('{V2}', VALUE_LOCALCDN_VERSION));
  1199. }
  1200.  
  1201. function init() {
  1202. // Nothing to do here
  1203. }
  1204.  
  1205. function v01_To_v02() {
  1206. const urls = LC.listurls();
  1207. for (const url of urls) {
  1208. if (url === KEY_LOCALCDN_VERSION) {continue;}
  1209. CDN[url] = {
  1210. url: url,
  1211. time: 0,
  1212. content: CDN[url]
  1213. };
  1214. }
  1215. }
  1216.  
  1217. function v02_To_v03() {
  1218. const urls = LC.listurls();
  1219. for (const url of urls) {
  1220. CDN[url].success = CDN[url].fail = 0;
  1221. }
  1222. }
  1223. }
  1224.  
  1225. function clearExpired() {
  1226. const resources = LC.list();
  1227. const time = (new Date()).getTime();
  1228.  
  1229. for (const resource of resources) {
  1230. expired(resource.time, time) && LC.delete(resource.url);
  1231. }
  1232. }
  1233.  
  1234. function expired(t1, t2) {
  1235. return (t1 - t2) > (LC.expire * 60 * 60 * 1000);
  1236. }
  1237.  
  1238. upgradeConfig();
  1239. clearExpired();
  1240. }
  1241.  
  1242. function requestText(url, callback, args=[], onfail=function(){}) {
  1243. const req = typeof(GM_xmlhttpRequest) === 'function' ? GM_xmlhttpRequest : Provides.GM_xmlhttpRequest;
  1244. req({
  1245. method: 'GET',
  1246. url: url,
  1247. responseType: 'text',
  1248. timeout: 45*1000,
  1249. onload: function(response) {
  1250. const text = response.responseText;
  1251. const argvs = [text].concat(args);
  1252. callback.apply(null, argvs);
  1253. },
  1254. onerror: onfail,
  1255. ontimeout: onfail,
  1256. onabort: onfail,
  1257. })
  1258. }
  1259.  
  1260. function AsyncManager() {
  1261. const AM = this;
  1262.  
  1263. // Ongoing xhr count
  1264. this.taskCount = 0;
  1265.  
  1266. // Whether generate finish events
  1267. let finishEvent = false;
  1268. Object.defineProperty(this, 'finishEvent', {
  1269. configurable: true,
  1270. enumerable: true,
  1271. get: () => (finishEvent),
  1272. set: (b) => {
  1273. finishEvent = b;
  1274. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  1275. }
  1276. });
  1277.  
  1278. // Add one task
  1279. this.add = () => (++AM.taskCount);
  1280.  
  1281. // Finish one task
  1282. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  1283. }
  1284.  
  1285. // Arguments: level=LogLevel.Info, logContent, asObject=false
  1286. // Needs one call "DoLog();" to get it initialized before using it!
  1287. function DoLog() {
  1288. const win = typeof(unsafeWindow) !== 'undefined' ? unsafeWindow : window;
  1289.  
  1290. // Global log levels set
  1291. win.LogLevel = {
  1292. None: 0,
  1293. Error: 1,
  1294. Success: 2,
  1295. Warning: 3,
  1296. Info: 4,
  1297. }
  1298. win.LogLevelMap = {};
  1299. win.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
  1300. win.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
  1301. win.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
  1302. win.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
  1303. win.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
  1304. win.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
  1305.  
  1306. // Current log level
  1307. DoLog.logLevel = win.isPY_DNG ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  1308.  
  1309. // Log counter
  1310. DoLog.logCount === undefined && (DoLog.logCount = 0);
  1311. if (++DoLog.logCount > 512) {
  1312. console.clear();
  1313. DoLog.logCount = 0;
  1314. }
  1315.  
  1316. // Get args
  1317. let level, logContent, asObject;
  1318. switch (arguments.length) {
  1319. case 1:
  1320. level = LogLevel.Info;
  1321. logContent = arguments[0];
  1322. asObject = false;
  1323. break;
  1324. case 2:
  1325. level = arguments[0];
  1326. logContent = arguments[1];
  1327. asObject = false;
  1328. break;
  1329. case 3:
  1330. level = arguments[0];
  1331. logContent = arguments[1];
  1332. asObject = arguments[2];
  1333. break;
  1334. default:
  1335. level = LogLevel.Info;
  1336. logContent = 'DoLog initialized.';
  1337. asObject = false;
  1338. break;
  1339. }
  1340.  
  1341. // Log when log level permits
  1342. if (level <= DoLog.logLevel) {
  1343. let msg = '%c' + LogLevelMap[level].prefix;
  1344. let subst = LogLevelMap[level].color;
  1345.  
  1346. if (asObject) {
  1347. msg += ' %o';
  1348. } else {
  1349. switch(typeof(logContent)) {
  1350. case 'string': msg += ' %s'; break;
  1351. case 'number': msg += ' %d'; break;
  1352. case 'object': msg += ' %o'; break;
  1353. }
  1354. }
  1355.  
  1356. console.log(msg, subst, logContent);
  1357. }
  1358. }
  1359. }
  1360.  
  1361. // Polyfill String.prototype.replaceAll
  1362. // replaceValue does NOT support regexp match groups($1, $2, etc.)
  1363. function polyfill_replaceAll() {
  1364. String.prototype.replaceAll = String.prototype.replaceAll ? String.prototype.replaceAll : PF_replaceAll;
  1365.  
  1366. function PF_replaceAll(searchValue, replaceValue) {
  1367. const str = String(this);
  1368.  
  1369. if (searchValue instanceof RegExp) {
  1370. const global = RegExp(searchValue, 'g');
  1371. if (/\$/.test(replaceValue)) {console.error('Error: Polyfilled String.protopype.replaceAll does support regexp groups');};
  1372. return str.replace(global, replaceValue);
  1373. } else {
  1374. return str.split(searchValue).join(replaceValue);
  1375. }
  1376. }
  1377. }
  1378.  
  1379. function randint(min, max) {
  1380. return Math.floor(Math.random() * (max - min + 1)) + min;
  1381. }
  1382.  
  1383. // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
  1384. function delItem(arr, delIndex) {
  1385. arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
  1386. return arr;
  1387. }
  1388. })();