Greasyfork 快捷編輯收藏

在GF腳本頁添加快速打開收藏集編輯頁面功能

目前為 2022-07-15 提交的版本,檢視 最新版本

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