Greasyfork 快捷编辑收藏

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

Fra 08.02.2022. Se den seneste versjonen.

  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
  10. // @description 在GF脚本页添加快速打开收藏集编辑页面功能
  11. // @description:zh-CN 在GF脚本页添加快速打开收藏集编辑页面功能
  12. // @description:zh-TW 在GF腳本頁添加快速打開收藏集編輯頁面功能
  13. // @description:en Add open script-set-edit-page button in GF script page
  14. // @author PY-DNG
  15. // @license GPL-3
  16. // @match http*://greatest.deepsurf.us/*
  17. // @icon https://api.iowen.cn/favicon/get.php?url=greatest.deepsurf.us
  18. // @grant GM_xmlhttpRequest
  19. // @grant GM_setValue
  20. // @grant GM_getValue
  21. // ==/UserScript==
  22.  
  23. (function() {
  24. 'use strict';
  25.  
  26. // Arguments: level=LogLevel.Info, logContent, asObject=false
  27. // Needs one call "DoLog();" to get it initialized before using it!
  28. function DoLog() {
  29. // Global log levels set
  30. window.LogLevel = {
  31. None: 0,
  32. Error: 1,
  33. Success: 2,
  34. Warning: 3,
  35. Info: 4,
  36. }
  37. window.LogLevelMap = {};
  38. window.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
  39. window.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
  40. window.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
  41. window.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
  42. window.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
  43. window.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
  44.  
  45. // Current log level
  46. DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  47.  
  48. // Log counter
  49. DoLog.logCount === undefined && (DoLog.logCount = 0);
  50. if (++DoLog.logCount > 512) {
  51. console.clear();
  52. DoLog.logCount = 0;
  53. }
  54.  
  55. // Get args
  56. let level, logContent, asObject;
  57. switch (arguments.length) {
  58. case 1:
  59. level = LogLevel.Info;
  60. logContent = arguments[0];
  61. asObject = false;
  62. break;
  63. case 2:
  64. level = arguments[0];
  65. logContent = arguments[1];
  66. asObject = false;
  67. break;
  68. case 3:
  69. level = arguments[0];
  70. logContent = arguments[1];
  71. asObject = arguments[2];
  72. break;
  73. default:
  74. level = LogLevel.Info;
  75. logContent = 'DoLog initialized.';
  76. asObject = false;
  77. break;
  78. }
  79.  
  80. // Log when log level permits
  81. if (level <= DoLog.logLevel) {
  82. let msg = '%c' + LogLevelMap[level].prefix;
  83. let subst = LogLevelMap[level].color;
  84.  
  85. if (asObject) {
  86. msg += ' %o';
  87. } else {
  88. switch(typeof(logContent)) {
  89. case 'string': msg += ' %s'; break;
  90. case 'number': msg += ' %d'; break;
  91. case 'object': msg += ' %o'; break;
  92. }
  93. }
  94.  
  95. console.log(msg, subst, logContent);
  96. }
  97. }
  98. DoLog();
  99.  
  100. bypassXB();
  101. GM_PolyFill('default');
  102.  
  103. // Inner consts with i18n
  104. const CONST = {
  105. Text: {
  106. 'zh-CN': {
  107. FavEdit: '收藏集:',
  108. Edit: '编辑',
  109. CopySID: '复制脚本ID'
  110. },
  111. 'zh-TW': {
  112. FavEdit: '收藏集:',
  113. Edit: '編輯',
  114. CopySID: '複製腳本ID'
  115. },
  116. 'en': {
  117. FavEdit: 'Add to/Remove from favorite list: ',
  118. Edit: 'Edit',
  119. CopySID: 'Copy-Script-ID'
  120. },
  121. 'default': {
  122. FavEdit: 'Add to/Remove from favorite list: ',
  123. Edit: 'Edit',
  124. CopySID: 'Copy-Script-ID'
  125. },
  126. }
  127. }
  128.  
  129. // Get i18n code
  130. let i18n = navigator.language;
  131. if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}
  132.  
  133. main()
  134. function main() {
  135. const HOST = getHost();
  136. const API = getAPI();
  137.  
  138. // Common actions
  139. commons();
  140.  
  141. // API-based actions
  142. switch(API[1]) {
  143. case "scripts":
  144. centerScript(API);
  145. break;
  146. default:
  147. DoLog('API is {}'.replace('{}', API));
  148. }
  149. }
  150.  
  151. function centerScript(API) {
  152. switch(API[3]) {
  153. case undefined:
  154. pageScript();
  155. break;
  156. case 'code':
  157. pageCode();
  158. break;
  159. case 'feedback':
  160. pageFeedback();
  161. break;
  162. }
  163. }
  164.  
  165. function commons() {
  166. // Your common actions here...
  167. }
  168.  
  169. function pageScript() {
  170. addFavPanel();
  171. }
  172.  
  173. function pageCode() {
  174. addFavPanel();
  175. }
  176.  
  177. function pageFeedback() {
  178. addFavPanel();
  179. }
  180.  
  181. function addFavPanel() {
  182. if (!getUserpage()) {return false;}
  183. GUI();
  184.  
  185. function GUI() {
  186. // Get elements
  187. const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
  188. const script_parent = script_after.parentElement;
  189.  
  190. // My elements
  191. const script_favorite = $C('div');
  192. script_favorite.id = 'script-favorite';
  193. script_favorite.style.margin = '0.75em 0';
  194. script_favorite.innerHTML = CONST.Text[i18n].FavEdit;
  195.  
  196. const favorite_groups = $C('select');
  197. favorite_groups.id = 'favorite-groups';
  198.  
  199. const stored_sets = GM_getValue('script-sets', {sets: []}).sets;
  200. for (const set of stored_sets) {
  201. // Make <option>
  202. const option = $C('option');
  203. option.innerText = set.name;
  204. option.value = set.linkedit;
  205. $A(favorite_groups, option);
  206. }
  207.  
  208. getScriptSets(function(sets) {
  209. clearChildnodes(favorite_groups);
  210. for (const set of sets) {
  211. // Make <option>
  212. const option = $C('option');
  213. option.innerText = set.name;
  214. option.value = set.linkedit;
  215. $A(favorite_groups, option);
  216. }
  217.  
  218. // Set edit-button.href
  219. favorite_edit.href = favorite_groups.value;
  220. })
  221. favorite_groups.addEventListener('change', function(e) {
  222. favorite_edit.href = favorite_groups.value;
  223. });
  224.  
  225. const favorite_edit = $C('a');
  226. favorite_edit.id = 'favorite-add';
  227. favorite_edit.innerHTML = CONST.Text[i18n].Edit;
  228. favorite_edit.style.margin = favorite_edit.style.margin = '0px 0.5em';
  229. favorite_edit.target = '_blank';
  230.  
  231. const favorite_copy = $C('a');
  232. favorite_copy.id = 'favorite-copy';
  233. favorite_copy.href = 'javascript: void(0);';
  234. favorite_copy.innerHTML = CONST.Text[i18n].CopySID;
  235. favorite_copy.addEventListener('click', function() {
  236. copyText(getStrSID());
  237. });
  238.  
  239. // Append to document
  240. $A(script_favorite, favorite_groups);
  241. $I(script_parent, script_favorite, script_after);
  242. $A(script_favorite, favorite_edit);
  243. $A(script_favorite, favorite_copy);
  244. }
  245. }
  246.  
  247. function getScriptSets(callback, args=[]) {
  248. const userpage = getUserpage();
  249. getDocument(userpage, function(oDom) {
  250. const user_script_sets = oDom.querySelector('#user-script-sets');
  251. const script_sets = [];
  252.  
  253. for (const li of user_script_sets.querySelectorAll('li')) {
  254. // Get fav info
  255. const name = li.childNodes[0].nodeValue.trimRight();
  256. const link = li.children[0].href;
  257. const linkedit = li.children[1].href;
  258.  
  259. // Append to script_sets
  260. script_sets.push({
  261. name: name,
  262. link: link,
  263. linkedit: linkedit
  264. });
  265. }
  266.  
  267. // Save to GM_storage
  268. GM_setValue('script-sets', {
  269. sets: script_sets,
  270. time: (new Date()).getTime(),
  271. version: '0.1'
  272. });
  273.  
  274. // callback
  275. callback.apply(null, [script_sets].concat(args));
  276. });
  277. }
  278.  
  279. function getUserpage() {
  280. const a = $('#nav-user-info>.user-profile-link>a');
  281. return a ? a.href : null;
  282. }
  283.  
  284. function getStrSID(url=location.href) {
  285. const API = getAPI(url);
  286. const strSID = API[2].match(/\d+/);
  287. return strSID;
  288. }
  289.  
  290. function getSID(url=location.href) {
  291. return Number(getStrSID(url));
  292. }
  293.  
  294. function $(e) {return document.querySelector(e);}
  295. function $C(e) {return document.createElement(e);}
  296. function $A(a,b) {return a.appendChild(b);}
  297. function $I(a,b,c) {return a.insertBefore(b,c);}
  298.  
  299. // Remove all childnodes from an element
  300. function clearChildnodes(element) {
  301. const cns = []
  302. for (const cn of element.childNodes) {
  303. cns.push(cn);
  304. }
  305. for (const cn of cns) {
  306. element.removeChild(cn);
  307. }
  308. }
  309.  
  310. // Just stopPropagation and preventDefault
  311. function destroyEvent(e) {
  312. if (!e) {return false;};
  313. if (!e instanceof Event) {return false;};
  314. e.stopPropagation();
  315. e.preventDefault();
  316. }
  317.  
  318. // Download and parse a url page into a html document(dom).
  319. // when xhr onload: callback.apply([dom, args])
  320. function getDocument(url, callback, args=[]) {
  321. GM_xmlhttpRequest({
  322. method : 'GET',
  323. url : url,
  324. responseType : 'blob',
  325. onloadstart : function() {
  326. DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
  327. },
  328. onload : function(response) {
  329. const htmlblob = response.response;
  330. parseDocument(htmlblob, callback, args);
  331. }
  332. })
  333. }
  334.  
  335. function parseDocument(htmlblob, callback, args=[]) {
  336. const reader = new FileReader();
  337. reader.onload = function(e) {
  338. const htmlText = reader.result;
  339. const dom = new DOMParser().parseFromString(htmlText, 'text/html');
  340. args = [dom].concat(args);
  341. callback.apply(null, args);
  342. //callback(dom, htmlText);
  343. }
  344. reader.readAsText(htmlblob, document.characterSet);
  345. }
  346.  
  347. // GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
  348. // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
  349. // (If the request is invalid, such as url === '', will return false and will NOT make this request)
  350. // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
  351. // Requires: function delItem(){...} & function uniqueIDMaker(){...}
  352. function GMXHRHook(maxXHR=5) {
  353. const GM_XHR = GM_xmlhttpRequest;
  354. const getID = uniqueIDMaker();
  355. let todoList = [], ongoingList = [];
  356. GM_xmlhttpRequest = safeGMxhr;
  357.  
  358. function safeGMxhr() {
  359. // Get an id for this request, arrange a request object for it.
  360. const id = getID();
  361. const request = {id: id, args: arguments, aborter: null};
  362.  
  363. // Deal onload function first
  364. dealEndingEvents(request);
  365.  
  366. /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
  367. // Stop invalid requests
  368. if (!validCheck(request)) {
  369. return false;
  370. }
  371. */
  372.  
  373. // Judge if we could start the request now or later?
  374. todoList.push(request);
  375. checkXHR();
  376. return makeAbortFunc(id);
  377.  
  378. // Decrease activeXHRCount while GM_XHR onload;
  379. function dealEndingEvents(request) {
  380. const e = request.args[0];
  381.  
  382. // onload event
  383. const oriOnload = e.onload;
  384. e.onload = function() {
  385. reqFinish(request.id);
  386. checkXHR();
  387. oriOnload ? oriOnload.apply(null, arguments) : function() {};
  388. }
  389.  
  390. // onerror event
  391. const oriOnerror = e.onerror;
  392. e.onerror = function() {
  393. reqFinish(request.id);
  394. checkXHR();
  395. oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
  396. }
  397.  
  398. // ontimeout event
  399. const oriOntimeout = e.ontimeout;
  400. e.ontimeout = function() {
  401. reqFinish(request.id);
  402. checkXHR();
  403. oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
  404. }
  405.  
  406. // onabort event
  407. const oriOnabort = e.onabort;
  408. e.onabort = function() {
  409. reqFinish(request.id);
  410. checkXHR();
  411. oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
  412. }
  413. }
  414.  
  415. // Check if the request is invalid
  416. function validCheck(request) {
  417. const e = request.args[0];
  418.  
  419. if (!e.url) {
  420. return false;
  421. }
  422.  
  423. return true;
  424. }
  425.  
  426. // Call a XHR from todoList and push the request object to ongoingList if called
  427. function checkXHR() {
  428. if (ongoingList.length >= maxXHR) {return false;};
  429. if (todoList.length === 0) {return false;};
  430. const req = todoList.shift();
  431. const reqArgs = req.args;
  432. const aborter = GM_XHR.apply(null, reqArgs);
  433. req.aborter = aborter;
  434. ongoingList.push(req);
  435. return req;
  436. }
  437.  
  438. // Make a function that aborts a certain request
  439. function makeAbortFunc(id) {
  440. return function() {
  441. let i;
  442.  
  443. // Check if the request haven't been called
  444. for (i = 0; i < todoList.length; i++) {
  445. const req = todoList[i];
  446. if (req.id === id) {
  447. // found this request: haven't been called
  448. delItem(todoList, i);
  449. return true;
  450. }
  451. }
  452.  
  453. // Check if the request is running now
  454. for (i = 0; i < ongoingList.length; i++) {
  455. const req = todoList[i];
  456. if (req.id === id) {
  457. // found this request: running now
  458. req.aborter();
  459. reqFinish(id);
  460. checkXHR();
  461. }
  462. }
  463.  
  464. // Oh no, this request is already finished...
  465. return false;
  466. }
  467. }
  468.  
  469. // Remove a certain request from ongoingList
  470. function reqFinish(id) {
  471. let i;
  472. for (i = 0; i < ongoingList.length; i++) {
  473. const req = ongoingList[i];
  474. if (req.id === id) {
  475. ongoingList = delItem(ongoingList, i);
  476. return true;
  477. }
  478. }
  479. return false;
  480. }
  481. }
  482. }
  483.  
  484. // Get a url argument from lacation.href
  485. // also recieve a function to deal the matched string
  486. // returns defaultValue if name not found
  487. // Args: name, dealFunc=(function(a) {return a;}), defaultValue=null
  488. function getUrlArgv(details) {
  489. typeof(details) === 'string' && (details = {name: details});
  490. typeof(details) === 'undefined' && (details = {});
  491. if (!details.name) {return null;};
  492.  
  493. const url = details.url ? details.url : location.href;
  494. const name = details.name ? details.name : '';
  495. const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
  496. const defaultValue = details.defaultValue ? details.defaultValue : null;
  497. const matcher = new RegExp(name + '=([^&]+)');
  498. const result = url.match(matcher);
  499. const argv = result ? dealFunc(result[1]) : defaultValue;
  500.  
  501. return argv;
  502. }
  503.  
  504. // Copy text to clipboard (needs to be called in an user event)
  505. function copyText(text) {
  506. // Create a new textarea for copying
  507. const newInput = document.createElement('textarea');
  508. document.body.appendChild(newInput);
  509. newInput.value = text;
  510. newInput.select();
  511. document.execCommand('copy');
  512. document.body.removeChild(newInput);
  513. }
  514.  
  515. // Append a style text to document(<head>) with a <style> element
  516. function addStyle(css, id) {
  517. const style = document.createElement("style");
  518. id && (style.id = id);
  519. style.textContent = css;
  520. for (const elm of document.querySelectorAll('#'+id)) {
  521. elm.parentElement && elm.parentElement.removeChild(elm);
  522. }
  523. document.head.appendChild(style);
  524. }
  525.  
  526. // File download function
  527. // details looks like the detail of GM_xmlhttpRequest
  528. // onload function will be called after file saved to disk
  529. function downloadFile(details) {
  530. if (!details.url || !details.name) {return false;};
  531.  
  532. // Configure request object
  533. const requestObj = {
  534. url: details.url,
  535. responseType: 'blob',
  536. onload: function(e) {
  537. // Save file
  538. saveFile(URL.createObjectURL(e.response), details.name);
  539.  
  540. // onload callback
  541. details.onload ? details.onload(e) : function() {};
  542. }
  543. }
  544. if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
  545. if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
  546. if (details.onerror ) {requestObj.onerror = details.onerror;};
  547. if (details.onabort ) {requestObj.onabort = details.onabort;};
  548. if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
  549. if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
  550.  
  551. // Send request
  552. GM_xmlhttpRequest(requestObj);
  553. }
  554.  
  555. // get '/' splited API array from a url
  556. function getAPI(url=location.href) {
  557. return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
  558. }
  559.  
  560. // get host part from a url(includes '^https://', '/$')
  561. function getHost(url=location.href) {
  562. const match = location.href.match(/https?:\/\/[^\/]+\//);
  563. return match ? match[0] : match;
  564. }
  565.  
  566. // Your code here...
  567. // Bypass xbrowser's useless GM_functions
  568. function bypassXB() {
  569. if (typeof(mbrowser) === 'object') {
  570. window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined;
  571. }
  572. }
  573.  
  574. // GM_Polyfill By PY-DNG
  575. // 2021.07.18 - 2021.07.19
  576. // Simply provides the following GM_functions using localStorage, XMLHttpRequest and window.open:
  577. // Returns object GM_POLYFILLED which has the following properties that shows you which GM_functions are actually polyfilled:
  578. // GM_setValue, GM_getValue, GM_deleteValue, GM_listValues, GM_xmlhttpRequest, GM_openInTab, GM_setClipboard, unsafeWindow(object)
  579. // All polyfilled GM_functions are accessable in window object/Global_Scope(only without Tempermonkey Sandboxing environment)
  580. function GM_PolyFill(name='default') {
  581. const GM_POLYFILL_KEY_STORAGE = 'GM_STORAGE_POLYFILL';
  582. let GM_POLYFILL_storage;
  583. const GM_POLYFILLED = {
  584. GM_setValue: true,
  585. GM_getValue: true,
  586. GM_deleteValue: true,
  587. GM_listValues: true,
  588. GM_xmlhttpRequest: true,
  589. GM_openInTab: true,
  590. GM_setClipboard: true,
  591. unsafeWindow: true,
  592. once: false
  593. }
  594.  
  595. // Ignore GM_PolyFill_Once
  596. window.GM_POLYFILLED && window.GM_POLYFILLED.once && (window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined);
  597.  
  598. GM_setValue_polyfill();
  599. GM_getValue_polyfill();
  600. GM_deleteValue_polyfill();
  601. GM_listValues_polyfill();
  602. GM_xmlhttpRequest_polyfill();
  603. GM_openInTab_polyfill();
  604. GM_setClipboard_polyfill();
  605. unsafeWindow_polyfill();
  606.  
  607. function GM_POLYFILL_getStorage() {
  608. let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
  609. gstorage = gstorage ? JSON.parse(gstorage) : {};
  610. let storage = gstorage[name] ? gstorage[name] : {};
  611. return storage;
  612. }
  613.  
  614. function GM_POLYFILL_saveStorage() {
  615. let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
  616. gstorage = gstorage ? JSON.parse(gstorage) : {};
  617. gstorage[name] = GM_POLYFILL_storage;
  618. localStorage.setItem(GM_POLYFILL_KEY_STORAGE, JSON.stringify(gstorage));
  619. }
  620.  
  621. // GM_setValue
  622. function GM_setValue_polyfill() {
  623. typeof (GM_setValue) === 'function' ? GM_POLYFILLED.GM_setValue = false: window.GM_setValue = PF_GM_setValue;;
  624.  
  625. function PF_GM_setValue(name, value) {
  626. GM_POLYFILL_storage = GM_POLYFILL_getStorage();
  627. name = String(name);
  628. GM_POLYFILL_storage[name] = value;
  629. GM_POLYFILL_saveStorage();
  630. }
  631. }
  632.  
  633. // GM_getValue
  634. function GM_getValue_polyfill() {
  635. typeof (GM_getValue) === 'function' ? GM_POLYFILLED.GM_getValue = false: window.GM_getValue = PF_GM_getValue;
  636.  
  637. function PF_GM_getValue(name, defaultValue) {
  638. GM_POLYFILL_storage = GM_POLYFILL_getStorage();
  639. name = String(name);
  640. if (GM_POLYFILL_storage.hasOwnProperty(name)) {
  641. return GM_POLYFILL_storage[name];
  642. } else {
  643. return defaultValue;
  644. }
  645. }
  646. }
  647.  
  648. // GM_deleteValue
  649. function GM_deleteValue_polyfill() {
  650. typeof (GM_deleteValue) === 'function' ? GM_POLYFILLED.GM_deleteValue = false: window.GM_deleteValue = PF_GM_deleteValue;
  651.  
  652. function PF_GM_deleteValue(name) {
  653. GM_POLYFILL_storage = GM_POLYFILL_getStorage();
  654. name = String(name);
  655. if (GM_POLYFILL_storage.hasOwnProperty(name)) {
  656. delete GM_POLYFILL_storage[name];
  657. GM_POLYFILL_saveStorage();
  658. }
  659. }
  660. }
  661.  
  662. // GM_listValues
  663. function GM_listValues_polyfill() {
  664. typeof (GM_listValues) === 'function' ? GM_POLYFILLED.GM_listValues = false: window.GM_listValues = PF_GM_listValues;
  665.  
  666. function PF_GM_listValues() {
  667. GM_POLYFILL_storage = GM_POLYFILL_getStorage();
  668. return Object.keys(GM_POLYFILL_storage);
  669. }
  670. }
  671.  
  672. // unsafeWindow
  673. function unsafeWindow_polyfill() {
  674. typeof (unsafeWindow) === 'object' ? GM_POLYFILLED.unsafeWindow = false: window.unsafeWindow = window;
  675. }
  676.  
  677. // GM_xmlhttpRequest
  678. // not supported properties of details: synchronous binary nocache revalidate context fetch
  679. // not supported properties of response(onload arguments[0]): finalUrl
  680. // ---!IMPORTANT!--- DOES NOT SUPPORT CROSS-ORIGIN REQUESTS!!!!! ---!IMPORTANT!---
  681. function GM_xmlhttpRequest_polyfill() {
  682. typeof (GM_xmlhttpRequest) === 'function' ? GM_POLYFILLED.GM_xmlhttpRequest = false: window.GM_xmlhttpRequest = PF_GM_xmlhttpRequest;
  683.  
  684. // details.synchronous is not supported as Tempermonkey
  685. function PF_GM_xmlhttpRequest(details) {
  686. const xhr = new XMLHttpRequest();
  687.  
  688. // open request
  689. const openArgs = [details.method, details.url, true];
  690. if (details.user && details.password) {
  691. openArgs.push(details.user);
  692. openArgs.push(details.password);
  693. }
  694. xhr.open.apply(xhr, openArgs);
  695.  
  696. // set headers
  697. if (details.headers) {
  698. for (const key of Object.keys(details.headers)) {
  699. xhr.setRequestHeader(key, details.headers[key]);
  700. }
  701. }
  702. details.cookie ? xhr.setRequestHeader('cookie', details.cookie) : function () {};
  703. details.anonymous ? xhr.setRequestHeader('cookie', '') : function () {};
  704.  
  705. // properties
  706. xhr.timeout = details.timeout;
  707. xhr.responseType = details.responseType;
  708. details.overrideMimeType ? xhr.overrideMimeType(details.overrideMimeType) : function () {};
  709.  
  710. // events
  711. xhr.onabort = details.onabort;
  712. xhr.onerror = details.onerror;
  713. xhr.onloadstart = details.onloadstart;
  714. xhr.onprogress = details.onprogress;
  715. xhr.onreadystatechange = details.onreadystatechange;
  716. xhr.ontimeout = details.ontimeout;
  717. xhr.onload = function (e) {
  718. const response = {
  719. readyState: xhr.readyState,
  720. status: xhr.status,
  721. statusText: xhr.statusText,
  722. responseHeaders: xhr.getAllResponseHeaders(),
  723. response: xhr.response
  724. };
  725. (details.responseType === '' || details.responseType === 'text') ? (response.responseText = xhr.responseText) : function () {};
  726. (details.responseType === '' || details.responseType === 'document') ? (response.responseXML = xhr.responseXML) : function () {};
  727. details.onload(response);
  728. }
  729.  
  730. // send request
  731. details.data ? xhr.send(details.data) : xhr.send();
  732.  
  733. return {
  734. abort: xhr.abort
  735. };
  736. }
  737. }
  738.  
  739. // NOTE: options(arg2) is NOT SUPPORTED! if provided, then will just be skipped.
  740. function GM_openInTab_polyfill() {
  741. typeof (GM_openInTab) === 'function' ? GM_POLYFILLED.GM_openInTab = false: window.GM_openInTab = PF_GM_openInTab;
  742.  
  743. function PF_GM_openInTab(url) {
  744. window.open(url);
  745. }
  746. }
  747.  
  748. // NOTE: needs to be called in an event handler function, and info(arg2) is NOT SUPPORTED!
  749. function GM_setClipboard_polyfill() {
  750. typeof (GM_setClipboard) === 'function' ? GM_POLYFILLED.GM_setClipboard = false: window.GM_setClipboard = PF_GM_setClipboard;
  751.  
  752. function PF_GM_setClipboard(text) {
  753. // Create a new textarea for copying
  754. const newInput = document.createElement('textarea');
  755. document.body.appendChild(newInput);
  756. newInput.value = text;
  757. newInput.select();
  758. document.execCommand('copy');
  759. document.body.removeChild(newInput);
  760. }
  761. }
  762.  
  763. return GM_POLYFILLED;
  764. }
  765.  
  766. // Makes a function that returns a unique ID number each time
  767. function uniqueIDMaker() {
  768. let id = 0;
  769. return makeID;
  770. function makeID() {
  771. id++;
  772. return id;
  773. }
  774. }
  775.  
  776. // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
  777. function delItem(arr, delIndex) {
  778. arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
  779. return arr;
  780. }
  781. })();