Greasyfork 快捷编辑收藏

在GF脚本页添加快速打开收藏集编辑页面功能

Versione datata 13/06/2022. Vedi la nuova versione l'ultima versione.

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