Greasyfork 快捷编辑收藏

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

目前为 2023-10-06 提交的版本。查看 最新版本

  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.6
  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 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAbBJREFUOE+Vk7GKGlEUhr8pAiKKDlqpCDpLUCzWBxCENBa+hBsL9wHsLWxXG4tNtcGH0MIiWopY7JSGEUWsbESwUDMw4Z7siLsZDbnlPff/7n/+e67G38sA6sAXIPVWXgA/gCdgfinRPuhfCoXCw3Q65XA4eLBl6zvw1S2eAZqmvTqOc5/NZhkMBqRSKWzbvgYxgbwquoAX4MGyLHK5HIlEgtFo9C+IOFEAo1gsWsvlUmyPx2MymYxAhsMh6XT6lpM7BXjWdf1xNpuRz+fl8GQywTAMGo0G1WpVnJxOJ692vinADPgcDAaZz+cCOR6PmKZJPB4XUb/fp1wuewF+KoBCf1JVBVE5dDodms3mWdDtdqlUKl6AX+8ALmS9XgtM0/5kvNlspKX9fv8RIgBp4bISCoXo9XqsVitKpRK6rrPb7STQ7XZ7eVRaeAYerz14OBxGOfL7/eIgmUwKzHEcJZEQ1eha1wBqPxqNihufzyeQWCzmtiPPqJYM0jWIyiISibBYLAgEAtTrdVqt1nmQXN0rcH/LicqmVqvRbrdN27bfjbKru+nk7ZD3Z7q4+b++82/YPKIrXsKZ3AAAAABJRU5ErkJggg==
  23. // @grant GM_xmlhttpRequest
  24. // @grant GM_setValue
  25. // @grant GM_getValue
  26. // ==/UserScript==
  27.  
  28. (function __MAIN__() {
  29. 'use strict';
  30.  
  31. // function DoLog() {}
  32. // Arguments: level=LogLevel.Info, logContent, trace=false
  33. const [LogLevel, DoLog] = (function() {
  34. const LogLevel = {
  35. None: 0,
  36. Error: 1,
  37. Success: 2,
  38. Warning: 3,
  39. Info: 4,
  40. };
  41.  
  42. return [LogLevel, DoLog];
  43. function DoLog() {
  44. // Get window
  45. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;
  46.  
  47. const LogLevelMap = {};
  48. LogLevelMap[LogLevel.None] = {
  49. prefix: '',
  50. color: 'color:#ffffff'
  51. }
  52. LogLevelMap[LogLevel.Error] = {
  53. prefix: '[Error]',
  54. color: 'color:#ff0000'
  55. }
  56. LogLevelMap[LogLevel.Success] = {
  57. prefix: '[Success]',
  58. color: 'color:#00aa00'
  59. }
  60. LogLevelMap[LogLevel.Warning] = {
  61. prefix: '[Warning]',
  62. color: 'color:#ffa500'
  63. }
  64. LogLevelMap[LogLevel.Info] = {
  65. prefix: '[Info]',
  66. color: 'color:#888888'
  67. }
  68. LogLevelMap[LogLevel.Elements] = {
  69. prefix: '[Elements]',
  70. color: 'color:#000000'
  71. }
  72.  
  73. // Current log level
  74. DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  75.  
  76. // Log counter
  77. DoLog.logCount === undefined && (DoLog.logCount = 0);
  78.  
  79. // Get args
  80. let [level, logContent, trace] = parseArgs([...arguments], [
  81. [2],
  82. [1,2],
  83. [1,2,3]
  84. ], [LogLevel.Info, 'DoLog initialized.', false]);
  85.  
  86. // Log when log level permits
  87. if (level <= DoLog.logLevel) {
  88. let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
  89. let subst = LogLevelMap[level].color;
  90.  
  91. switch (typeof(logContent)) {
  92. case 'string':
  93. msg += '%s';
  94. break;
  95. case 'number':
  96. msg += '%d';
  97. break;
  98. default:
  99. msg += '%o';
  100. break;
  101. }
  102.  
  103. if (++DoLog.logCount > 512) {
  104. console.clear();
  105. DoLog.logCount = 0;
  106. }
  107. console[trace ? 'trace' : 'log'](msg, subst, logContent);
  108. }
  109. }
  110. }) ();
  111.  
  112. const CONST = {
  113. Text: {
  114. 'zh-CN': {
  115. FavEdit: '收藏集:',
  116. Add: '加入此集',
  117. Edit: '手动编辑',
  118. CopySID: '复制脚本ID',
  119. Working: ['正在添加...', '就快好了...'],
  120. Error: {
  121. Unknown: '未知错误'
  122. }
  123. },
  124. 'zh-TW': {
  125. FavEdit: '收藏集:',
  126. Add: '加入此集',
  127. Edit: '手動編輯',
  128. CopySID: '複製腳本ID',
  129. Working: ['正在添加...', '就快好了...'],
  130. Error: {
  131. Unknown: '未知錯誤'
  132. }
  133. },
  134. 'en': {
  135. FavEdit: 'Add to/Remove from favorite list: ',
  136. Add: 'Add',
  137. Edit: 'Edit Manually',
  138. CopySID: 'Copy-Script-ID',
  139. Working: ['Working...', 'Just a moment...'],
  140. Error: {
  141. Unknown: 'Unknown Error'
  142. }
  143. },
  144. 'default': {
  145. FavEdit: 'Add to/Remove from favorite list: ',
  146. Add: 'Add',
  147. Edit: 'Edit Manually',
  148. CopySID: 'Copy-Script-ID',
  149. Working: ['Working...', 'Just a moment...'],
  150. Error: {
  151. Unknown: 'Unknown Error'
  152. }
  153. },
  154. }
  155. }
  156.  
  157. // Get i18n code
  158. let i18n = navigator.language;
  159. if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}
  160.  
  161. main()
  162. function main() {
  163. const HOST = getHost();
  164. const API = getAPI();
  165.  
  166. // Common actions
  167. commons();
  168.  
  169. // API-based actions
  170. switch(API[1]) {
  171. case "scripts":
  172. API[2] && centerScript(API);
  173. break;
  174. default:
  175. DoLog('API is {}'.replace('{}', API));
  176. }
  177. }
  178.  
  179. function centerScript(API) {
  180. switch(API[3]) {
  181. case undefined:
  182. pageScript();
  183. break;
  184. case 'code':
  185. pageCode();
  186. break;
  187. case 'feedback':
  188. pageFeedback();
  189. break;
  190. }
  191. }
  192.  
  193. function commons() {
  194. // Your common actions here...
  195. }
  196.  
  197. function pageScript() {
  198. addFavPanel();
  199. }
  200.  
  201. function pageCode() {
  202. addFavPanel();
  203. }
  204.  
  205. function pageFeedback() {
  206. addFavPanel();
  207. }
  208.  
  209. function addFavPanel() {
  210. if (!getUserpage()) {return false;}
  211. GUI();
  212.  
  213. function GUI() {
  214. // Get elements
  215. const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
  216. const script_parent = script_after.parentElement;
  217.  
  218. // My elements
  219. const script_favorite = $CrE('div');
  220. script_favorite.id = 'script-favorite';
  221. script_favorite.style.margin = '0.75em 0';
  222. script_favorite.innerHTML = CONST.Text[i18n].FavEdit;
  223.  
  224. const favorite_groups = $CrE('select');
  225. favorite_groups.id = 'favorite-groups';
  226.  
  227. const stored_sets = GM_getValue('script-sets', {sets: []}).sets;
  228. for (const set of stored_sets) {
  229. // Make <option>
  230. const option = $CrE('option');
  231. option.innerText = set.name;
  232. option.value = set.linkedit;
  233. $APD(favorite_groups, option);
  234. }
  235. adjustWidth();
  236.  
  237. getScriptSets(function(sets) {
  238. clearChildnodes(favorite_groups);
  239. for (const set of sets) {
  240. // Make <option>
  241. const option = $CrE('option');
  242. option.innerText = set.name;
  243. option.value = set.linkedit;
  244. $APD(favorite_groups, option);
  245. }
  246. adjustWidth();
  247.  
  248. // Set edit-button.href
  249. favorite_edit.href = favorite_groups.value;
  250. })
  251. favorite_groups.addEventListener('change', function(e) {
  252. favorite_edit.href = favorite_groups.value;
  253. });
  254.  
  255. const favorite_add = $CrE('a');
  256. favorite_add.id = 'favorite-add';
  257. favorite_add.innerHTML = CONST.Text[i18n].Add;
  258. favorite_add.style.margin = favorite_add.style.margin = '0px 0.5em';
  259. favorite_add.href = 'javascript:void(0);'
  260. favorite_add.addEventListener('click', function(e) {
  261. addFav();
  262. });
  263.  
  264. const favorite_edit = $CrE('a');
  265. favorite_edit.id = 'favorite-edit';
  266. favorite_edit.innerHTML = CONST.Text[i18n].Edit;
  267. favorite_edit.style.margin = favorite_edit.style.margin = '0px 0.5em';
  268. favorite_edit.target = '_blank';
  269.  
  270. const favorite_copy = $CrE('a');
  271. favorite_copy.id = 'favorite-copy';
  272. favorite_copy.href = 'javascript: void(0);';
  273. favorite_copy.innerHTML = CONST.Text[i18n].CopySID;
  274. favorite_copy.addEventListener('click', function() {
  275. copyText(getStrSID());
  276. });
  277.  
  278. // Append to document
  279. $APD(script_favorite, favorite_groups);
  280. script_parent.insertBefore(script_favorite, script_after);
  281. $APD(script_favorite, favorite_add);
  282. $APD(script_favorite, favorite_edit);
  283. $APD(script_favorite, favorite_copy);
  284.  
  285. function adjustWidth() {
  286. favorite_groups.style.width = Math.max.apply(null, Array.from(favorite_groups.children).map((o) => (o.innerText.length))).toString() + 'em';
  287. favorite_groups.style.maxWidth = '40vw';
  288. }
  289.  
  290. function addFav() {
  291. const iframe = $CrE('iframe');
  292. iframe.style.width = iframe.style.height = iframe.style.border = '0';
  293. iframe.addEventListener('load', edit_onload, {once: true});
  294. iframe.src = favorite_groups.value;
  295. $APD(document.body, iframe);
  296. displayNotice(CONST.Text[i18n].Working[0]);
  297.  
  298. function edit_onload() {
  299. const oDom = iframe.contentDocument;
  300. const input = $CrE('input');
  301. input.value = getStrSID();
  302. input.name = 'scripts-included[]';
  303. input.type = 'hidden';
  304. $APD($(oDom, '#script-set-scripts'), input);
  305. $(oDom, 'button[name="save"]').click();
  306. iframe.addEventListener('load', finish_onload, {once: true});
  307. displayNotice(CONST.Text[i18n].Working[1]);
  308. }
  309.  
  310. function finish_onload() {
  311. const status = $(iframe.contentDocument, 'p.notice');
  312. const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
  313. displayNotice(status_text);
  314. iframe.parentElement.removeChild(iframe);
  315. }
  316.  
  317. function displayNotice(text) {
  318. const notice = $CrE('p');
  319. notice.classList.add('notice');
  320. notice.id = 'fav-notice';
  321. notice.innerText = text;
  322. const old_notice = $('#fav-notice');
  323. old_notice && old_notice.parentElement.removeChild(old_notice);
  324. $('#script-content').insertAdjacentElement('afterbegin', notice);
  325. }
  326. }
  327. }
  328. }
  329.  
  330. function getScriptSets(callback, args=[]) {
  331. const userpage = getUserpage();
  332. getDocument(userpage, function(oDom) {
  333. const user_script_sets = oDom.querySelector('#user-script-sets');
  334. const script_sets = [];
  335.  
  336. for (const li of user_script_sets.querySelectorAll('li')) {
  337. // Get fav info
  338. const name = li.childNodes[0].nodeValue.trimRight();
  339. const link = li.children[0].href;
  340. 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';
  341.  
  342. // Append to script_sets
  343. script_sets.push({
  344. name: name,
  345. link: link,
  346. linkedit: linkedit
  347. });
  348. }
  349.  
  350. // Save to GM_storage
  351. GM_setValue('script-sets', {
  352. sets: script_sets,
  353. time: (new Date()).getTime(),
  354. version: '0.1'
  355. });
  356.  
  357. // callback
  358. callback.apply(null, [script_sets].concat(args));
  359. });
  360. }
  361.  
  362. function getUserpage() {
  363. const a = $('#nav-user-info>.user-profile-link>a');
  364. return a ? a.href : null;
  365. }
  366.  
  367. function getStrSID(url=location.href) {
  368. const API = getAPI(url);
  369. const strSID = API[2].match(/\d+/);
  370. return strSID;
  371. }
  372.  
  373. function getSID(url=location.href) {
  374. return Number(getStrSID(url));
  375. }
  376. // Basic functions
  377. // querySelector
  378. function $() {
  379. switch(arguments.length) {
  380. case 2:
  381. return arguments[0].querySelector(arguments[1]);
  382. break;
  383. default:
  384. return document.querySelector(arguments[0]);
  385. }
  386. }
  387. // querySelectorAll
  388. function $All() {
  389. switch(arguments.length) {
  390. case 2:
  391. return arguments[0].querySelectorAll(arguments[1]);
  392. break;
  393. default:
  394. return document.querySelectorAll(arguments[0]);
  395. }
  396. }
  397. // createElement
  398. function $CrE() {
  399. switch(arguments.length) {
  400. case 2:
  401. return arguments[0].createElement(arguments[1]);
  402. break;
  403. default:
  404. return document.createElement(arguments[0]);
  405. }
  406. }
  407. function $APD(a,b) {return a.appendChild(b);}
  408. // Object1[prop] ==> Object2[prop]
  409. function copyProp(obj1, obj2, prop) {obj1[prop] !== undefined && (obj2[prop] = obj1[prop]);}
  410. function copyProps(obj1, obj2, props) {props.forEach((prop) => (copyProp(obj1, obj2, prop)));}
  411.  
  412. // Just stopPropagation and preventDefault
  413. function destroyEvent(e) {
  414. if (!e) {return false;};
  415. if (!e instanceof Event) {return false;};
  416. e.stopPropagation();
  417. e.preventDefault();
  418. }
  419.  
  420. // Remove all childnodes from an element
  421. function clearChildnodes(element) {
  422. const cns = []
  423. for (const cn of element.childNodes) {
  424. cns.push(cn);
  425. }
  426. for (const cn of cns) {
  427. element.removeChild(cn);
  428. }
  429. }
  430.  
  431. // Download and parse a url page into a html document(dom).
  432. // when xhr onload: callback.apply([dom, args])
  433. function getDocument(url, callback, args=[]) {
  434. GM_xmlhttpRequest({
  435. method : 'GET',
  436. url : url,
  437. responseType : 'blob',
  438. onloadstart : function() {
  439. DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
  440. },
  441. onload : function(response) {
  442. const htmlblob = response.response;
  443. parseDocument(htmlblob, callback, args);
  444. }
  445. })
  446. }
  447.  
  448. function parseDocument(htmlblob, callback, args=[]) {
  449. const reader = new FileReader();
  450. reader.onload = function(e) {
  451. const htmlText = reader.result;
  452. const dom = new DOMParser().parseFromString(htmlText, 'text/html');
  453. args = [dom].concat(args);
  454. callback.apply(null, args);
  455. //callback(dom, htmlText);
  456. }
  457. reader.readAsText(htmlblob, document.characterSet);
  458. }
  459.  
  460. // Get a url argument from lacation.href
  461. // also recieve a function to deal the matched string
  462. // returns defaultValue if name not found
  463. // Args: {url=location.href, name, dealFunc=((a)=>{return a;}), defaultValue=null} or 'name'
  464. function getUrlArgv(details) {
  465. typeof(details) === 'string' && (details = {name: details});
  466. typeof(details) === 'undefined' && (details = {});
  467. if (!details.name) {return null;};
  468.  
  469. const url = details.url ? details.url : location.href;
  470. const name = details.name ? details.name : '';
  471. const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
  472. const defaultValue = details.defaultValue ? details.defaultValue : null;
  473. const matcher = new RegExp('[\\?&]' + name + '=([^&#]+)');
  474. const result = url.match(matcher);
  475. const argv = result ? dealFunc(result[1]) : defaultValue;
  476.  
  477. return argv;
  478. }
  479.  
  480. // Copy text to clipboard (needs to be called in an user event)
  481. function copyText(text) {
  482. // Create a new textarea for copying
  483. const newInput = document.createElement('textarea');
  484. document.body.appendChild(newInput);
  485. newInput.value = text;
  486. newInput.select();
  487. document.execCommand('copy');
  488. document.body.removeChild(newInput);
  489. }
  490.  
  491. // Append a style text to document(<head>) with a <style> element
  492. function addStyle(css, id) {
  493. const style = document.createElement("style");
  494. id && (style.id = id);
  495. style.textContent = css;
  496. for (const elm of document.querySelectorAll('#'+id)) {
  497. elm.parentElement && elm.parentElement.removeChild(elm);
  498. }
  499. document.head.appendChild(style);
  500. }
  501.  
  502. // get '/' splited API array from a url
  503. function getAPI(url=location.href) {
  504. return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
  505. }
  506.  
  507. // get host part from a url(includes '^https://', '/$')
  508. function getHost(url=location.href) {
  509. const match = location.href.match(/https?:\/\/[^\/]+\//);
  510. return match ? match[0] : match;
  511. }
  512.  
  513. function AsyncManager() {
  514. const AM = this;
  515.  
  516. // Ongoing xhr count
  517. this.taskCount = 0;
  518.  
  519. // Whether generate finish events
  520. let finishEvent = false;
  521. Object.defineProperty(this, 'finishEvent', {
  522. configurable: true,
  523. enumerable: true,
  524. get: () => (finishEvent),
  525. set: (b) => {
  526. finishEvent = b;
  527. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  528. }
  529. });
  530.  
  531. // Add one task
  532. this.add = () => (++AM.taskCount);
  533.  
  534. // Finish one task
  535. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  536. }
  537.  
  538. function randint(min, max) {
  539. return Math.floor(Math.random() * (max - min + 1)) + min;
  540. }
  541.  
  542. function parseArgs(args, rules, defaultValues=[]) {
  543. // args and rules should be array, but not just iterable (string is also iterable)
  544. if (!Array.isArray(args) || !Array.isArray(rules)) {
  545. throw new TypeError('parseArgs: args and rules should be array')
  546. }
  547.  
  548. // fill rules[0]
  549. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  550.  
  551. // max arguments length
  552. const count = rules.length - 1;
  553.  
  554. // args.length must <= count
  555. if (args.length > count) {
  556. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  557. }
  558.  
  559. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  560. for (let i = 1; i <= count; i++) {
  561. const rule = rules[i];
  562. if (Array.isArray(rule)) {
  563. if (rule.length !== i) {
  564. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  565. }
  566. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  567. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  568. }
  569. } else if (typeof rule !== 'function') {
  570. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  571. }
  572. }
  573.  
  574. // Parse
  575. const rule = rules[args.length];
  576. let parsed;
  577. if (Array.isArray(rule)) {
  578. parsed = [...defaultValues];
  579. for (let i = 0; i < rule.length; i++) {
  580. parsed[rule[i]-1] = args[i];
  581. }
  582. } else {
  583. parsed = rule(args, defaultValues);
  584. }
  585. return parsed;
  586. }
  587. })();