Greasy Fork is available in English.

Userscript App Core

Userscript App Core For Userscript Web Apps

As of 27.09.2022. See ბოლო ვერსია.

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