Greasyfork 快捷编辑收藏

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

目前为 2022-07-18 提交的版本。查看 最新版本

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