Greasyfork 快捷编辑收藏

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

As of 2023-11-10. See the latest version.

  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. // @name:fr Greasyfork Set Edit+
  10. // @namespace Greasyfork-Favorite
  11. // @version 0.1.9.2
  12. // @description 在GF脚本页添加快速打开收藏集编辑页面功能
  13. // @description:zh-CN 在GF脚本页添加快速打开收藏集编辑页面功能
  14. // @description:zh-TW 在GF腳本頁添加快速打開收藏集編輯頁面功能
  15. // @description:en Add / Remove script into / from script set directly in GF script info page
  16. // @description:en-US Add / Remove script into / from script set directly in GF script info page
  17. // @description:fr Ajouter un script à un jeu de scripts / supprimer un script d'un jeu de scripts directement sur la page d'informations sur les scripts GF
  18. // @author PY-DNG
  19. // @license GPL-3
  20. // @match http*://*.greatest.deepsurf.us/*
  21. // @match http*://*.sleazyfork.org/*
  22. // @require https://greatest.deepsurf.us/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1226884
  23. // @require https://greatest.deepsurf.us/scripts/460385-gm-web-hooks/code/script.js?version=1221394
  24. // @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==
  25. // @grant GM_xmlhttpRequest
  26. // @grant GM_setValue
  27. // @grant GM_getValue
  28. // ==/UserScript==
  29.  
  30. /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */
  31. /* global GMXHRHook GMDLHook */
  32.  
  33. (function __MAIN__() {
  34. 'use strict';
  35.  
  36. const CONST = {
  37. Text: {
  38. 'zh-CN': {
  39. FavEdit: '收藏集:',
  40. Add: '加入此集',
  41. Remove: '移出此集',
  42. Edit: '手动编辑',
  43. CopySID: '复制脚本ID',
  44. Working: ['工作中...', '就快好了...'],
  45. InSetStatus: ['[ ]', '[✔]'],
  46. Error: {
  47. AlreadyExist: '脚本已经在此收藏集中了',
  48. NotExist: '脚本不在此收藏集中',
  49. Unknown: '未知错误'
  50. }
  51. },
  52. 'zh-TW': {
  53. FavEdit: '收藏集:',
  54. Add: '加入此集',
  55. Remove: '移出此集',
  56. Edit: '手動編輯',
  57. CopySID: '複製腳本ID',
  58. Working: ['工作中...', '就快好了...'],
  59. InSetStatus: ['[ ]', '[✔]'],
  60. Error: {
  61. AlreadyExist: '腳本已經在此收藏集中了',
  62. NotExist: '腳本不在此收藏集中',
  63. Unknown: '未知錯誤'
  64. }
  65. },
  66. 'en': {
  67. FavEdit: 'Add to/Remove from favorite list: ',
  68. Add: 'Add',
  69. Remove: 'Remove',
  70. Edit: 'Edit Manually',
  71. CopySID: 'Copy-Script-ID',
  72. Working: ['Working...', 'Just a moment...'],
  73. InSetStatus: ['[ ]', '[✔]'],
  74. Error: {
  75. AlreadyExist: 'Script is already in set',
  76. NotExist: 'Script is not in set yet',
  77. Unknown: 'Unknown Error'
  78. }
  79. },
  80. 'default': {
  81. FavEdit: 'Add to/Remove from favorite list: ',
  82. Add: 'Add',
  83. Remove: 'Remove',
  84. Edit: 'Edit Manually',
  85. CopySID: 'Copy-Script-ID',
  86. Working: ['Working...', 'Just a moment...'],
  87. InSetStatus: ['[ ]', '[✔]'],
  88. Error: {
  89. AlreadyExist: 'Script is already in set',
  90. NotExist: 'Script is not in set yet',
  91. Unknown: 'Unknown Error'
  92. }
  93. },
  94. }
  95. }
  96.  
  97. // Get i18n code
  98. let i18n = navigator.language;
  99. if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}
  100.  
  101. main()
  102. function main() {
  103. const HOST = getHost();
  104. const API = getAPI();
  105.  
  106. // Common actions
  107. commons();
  108.  
  109. // API-based actions
  110. switch(API[1]) {
  111. case "scripts":
  112. API[2] && centerScript(API);
  113. break;
  114. default:
  115. DoLog('API is {}'.replace('{}', API));
  116. }
  117. }
  118.  
  119. function centerScript(API) {
  120. switch(API[3]) {
  121. case undefined:
  122. pageScript();
  123. break;
  124. case 'code':
  125. pageCode();
  126. break;
  127. case 'feedback':
  128. pageFeedback();
  129. break;
  130. }
  131. }
  132.  
  133. function commons() {
  134. // Your common actions here...
  135. GMXHRHook(5);
  136. }
  137.  
  138. function pageScript() {
  139. addFavPanel();
  140. }
  141.  
  142. function pageCode() {
  143. addFavPanel();
  144. }
  145.  
  146. function pageFeedback() {
  147. addFavPanel();
  148. }
  149.  
  150. function addFavPanel() {
  151. if (!getUserpage()) {return false;}
  152. GUI();
  153.  
  154. function GUI() {
  155. // Get elements
  156. const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
  157. const script_parent = script_after.parentElement;
  158.  
  159. // My elements
  160. const script_favorite = $CrE('div');
  161. script_favorite.id = 'script-favorite';
  162. script_favorite.style.margin = '0.75em 0';
  163. script_favorite.innerHTML = CONST.Text[i18n].FavEdit;
  164.  
  165. const favorite_groups = $CrE('select');
  166. favorite_groups.id = 'favorite-groups';
  167.  
  168. const stored_sets = GM_getValue('script-sets', {sets: []}).sets;
  169. for (const set of stored_sets) {
  170. // Make <option>
  171. const option = $CrE('option');
  172. option.innerText = set.name;
  173. option.value = set.linkedit;
  174. $APD(favorite_groups, option);
  175. }
  176. adjustWidth();
  177.  
  178. getScriptSets(function(sets) {
  179. clearChildnodes(favorite_groups);
  180. for (const set of sets) {
  181. // Make <option>
  182. const option = set.elmOption = $CrE('option');
  183. option.innerText = set.name;
  184. option.value = set.linkedit;
  185. $APD(favorite_groups, option);
  186. }
  187. adjustWidth();
  188.  
  189. // Set edit-button.href
  190. favorite_edit.href = favorite_groups.value;
  191.  
  192. // Check script in-set status
  193. getInSets(sets, getStrSID(), inSets => {
  194. sets.forEach(set => {
  195. const inSet = inSets.includes(set);
  196. set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[inSet+0]} ${set.name}`;
  197. });
  198. adjustWidth();
  199. });
  200. })
  201. favorite_groups.addEventListener('change', function(e) {
  202. favorite_edit.href = favorite_groups.value;
  203. });
  204.  
  205. const favorite_add = $CrE('a');
  206. favorite_add.id = 'favorite-add';
  207. favorite_add.innerHTML = CONST.Text[i18n].Add;
  208. favorite_add.style.margin = favorite_add.style.margin = '0px 0.5em';
  209. favorite_add.href = 'javascript:void(0);'
  210. favorite_add.addEventListener('click', e => addFav());
  211.  
  212. const favorite_remove = $CrE('a');
  213. favorite_remove.id = 'favorite-add';
  214. favorite_remove.innerHTML = CONST.Text[i18n].Remove;
  215. favorite_remove.style.margin = favorite_remove.style.margin = '0px 0.5em';
  216. favorite_remove.href = 'javascript:void(0);'
  217. favorite_remove.addEventListener('click', e => removeFav());
  218.  
  219. const favorite_edit = $CrE('a');
  220. favorite_edit.id = 'favorite-edit';
  221. favorite_edit.innerHTML = CONST.Text[i18n].Edit;
  222. favorite_edit.style.margin = favorite_edit.style.margin = '0px 0.5em';
  223. favorite_edit.target = '_blank';
  224.  
  225. const favorite_copy = $CrE('a');
  226. favorite_copy.id = 'favorite-copy';
  227. favorite_copy.href = 'javascript: void(0);';
  228. favorite_copy.innerHTML = CONST.Text[i18n].CopySID;
  229. favorite_copy.addEventListener('click', function() {
  230. copyText(getStrSID());
  231. });
  232.  
  233. // Append to document
  234. $APD(script_favorite, favorite_groups);
  235. script_parent.insertBefore(script_favorite, script_after);
  236. [favorite_add, favorite_remove, favorite_edit, favorite_copy].forEach(button => $APD(script_favorite, button));
  237.  
  238. function adjustWidth() {
  239. favorite_groups.style.width = Math.max.apply(null, Array.from(favorite_groups.children).map((o) => (o.innerText.length))).toString() + 'em';
  240. favorite_groups.style.maxWidth = '40vw';
  241. }
  242.  
  243. function addFav() {
  244. const option = favorite_groups.selectedOptions[0];
  245. const set = GM_getValue('script-sets').sets.find(set => set.linkedit === option.value);
  246. const url = favorite_groups.value;
  247.  
  248. displayNotice(CONST.Text[i18n].Working[0]);
  249. modifyFav(favorite_groups.value, oDom => {
  250. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === getStrSID());
  251. if (existingInput) {
  252. displayNotice(CONST.Text[i18n].Error.AlreadyExist);
  253. option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
  254. return false;
  255. }
  256.  
  257. const input = $CrE('input');
  258. input.value = getStrSID();
  259. input.name = 'scripts-included[]';
  260. input.type = 'hidden';
  261. $APD($(oDom, '#script-set-scripts'), input);
  262. displayNotice(CONST.Text[i18n].Working[1]);
  263. }, oDom => {
  264. const status = $(oDom, 'p.notice');
  265. const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
  266. displayNotice(status_text);
  267. option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
  268. }, onerror);
  269. }
  270.  
  271. function removeFav() {
  272. const option = favorite_groups.selectedOptions[0];
  273. const set = GM_getValue('script-sets').sets.find(set => set.linkedit === option.value);
  274. const url = favorite_groups.value;
  275.  
  276. displayNotice(CONST.Text[i18n].Working[0]);
  277. modifyFav(favorite_groups.value, oDom => {
  278. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === getStrSID());
  279. if (!existingInput) {
  280. displayNotice(CONST.Text[i18n].Error.NotExist);
  281. option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
  282. return false;
  283. }
  284.  
  285. existingInput.remove();
  286. displayNotice(CONST.Text[i18n].Working[1]);
  287. }, oDom => {
  288. const status = $(oDom, 'p.notice');
  289. const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
  290. displayNotice(status_text);
  291. option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
  292. }, onerror);
  293. }
  294.  
  295. function modifyFav(url, editCallback, finishCallback, onerror) {
  296. getDocument(url, oDom => {
  297. if (editCallback(oDom) === false) {
  298. return false;
  299. }
  300. const form = $(oDom, '.change-script-set');
  301. const data = new FormData(form);
  302. data.append('save', '1');
  303.  
  304. GM_xmlhttpRequest({
  305. method: 'POST',
  306. url: toAbsoluteURL(form.getAttribute('action')),
  307. data,
  308. responseType: 'blob',
  309. onload: response => parseDocument(response.response, oDom => finishCallback(oDom)),
  310. onerror
  311. });
  312. /*
  313. const xhr = new XMLHttpRequest();
  314. xhr.open('POST', toAbsoluteURL(form.getAttribute('action')));
  315. xhr.responseType = 'blob';
  316. xhr.onload = e => parseDocument(xhr.response, oDom => finishCallback(oDom));
  317. xhr.onerror = onerror;
  318. xhr.send(data);
  319. */
  320. });
  321. }
  322.  
  323. function onerror() {
  324. displayNotice(CONST.Text[i18n].Error.Unknown);
  325. }
  326.  
  327. function displayNotice(text) {
  328. const notice = $CrE('p');
  329. notice.classList.add('notice');
  330. notice.id = 'fav-notice';
  331. notice.innerText = text;
  332. const old_notice = $('#fav-notice');
  333. old_notice && old_notice.parentElement.removeChild(old_notice);
  334. $('#script-content').insertAdjacentElement('afterbegin', notice);
  335. }
  336. }
  337. }
  338.  
  339. function getScriptSets(callback, args=[]) {
  340. const userpage = getUserpage();
  341. getDocument(userpage, function(oDom) {
  342. /*
  343. const user_script_sets = oDom.querySelector('#user-script-sets');
  344. const script_sets = [];
  345.  
  346. for (const li of user_script_sets.querySelectorAll('li')) {
  347. // Get fav info
  348. const name = li.childNodes[0].nodeValue.trimRight();
  349. const link = li.children[0].href;
  350. const linkedit = li.children[1] ? li.children[1].href : 'https://greatest.deepsurf.us/' + $('#language-selector-locale').value + '/users/' + $('#nav-user-info>.user-profile-link>a').href.match(/[a-zA-Z\-]+\/users\/([^\/]*)/)[1] + '/sets/' + li.children[0].href.match(/[\?&]set=(\d+)/)[1] + '/edit';
  351.  
  352. // Append to script_sets
  353. script_sets.push({
  354. name: name,
  355. link: link,
  356. linkedit: linkedit
  357. });
  358. }
  359. */
  360. const script_sets = Array.from($(oDom, 'ul#user-script-sets').children).map(li => ({
  361. name: li.children[0].innerText,
  362. link: li.children[0].href,
  363. linkedit: li.children[1].href
  364. }));
  365.  
  366. // Save to GM_storage
  367. GM_setValue('script-sets', {
  368. sets: script_sets,
  369. time: (new Date()).getTime(),
  370. version: '0.2'
  371. });
  372.  
  373. // callback
  374. callback.apply(null, [script_sets].concat(args));
  375. });
  376. }
  377.  
  378. function getUserpage() {
  379. const a = $('#nav-user-info>.user-profile-link>a');
  380. return a ? a.href : null;
  381. }
  382.  
  383. function getInSet(set, sid, callback) {
  384. sid = sid.toString();
  385. getDocument(set.linkedit, oDom => {
  386. const inSet = [...$(oDom, '#script-set-scripts').children].some(input => input.value === sid);
  387. callback(inSet);
  388. });
  389. }
  390.  
  391. function getInSets(sets, sid, callback) {
  392. const AM = new AsyncManager();
  393. const inSets = [];
  394. for (const set of sets) {
  395. AM.add();
  396. getInSet(set, sid, inSet => {
  397. inSet && inSets.push(set);
  398. AM.finish();
  399. });
  400. }
  401. AM.onfinish = e => {
  402. callback(inSets);
  403. };
  404. AM.finishEvent = true;
  405. }
  406.  
  407. function getStrSID(url=location.href) {
  408. const API = getAPI(url);
  409. const strSID = API[2].match(/\d+/)[0];
  410. return strSID;
  411. }
  412.  
  413. function getSID(url=location.href) {
  414. return Number(getStrSID(url));
  415. }
  416. // Basic functions
  417. function $APD(a,b) {return a.appendChild(b);}
  418.  
  419. // Remove all childnodes from an element
  420. function clearChildnodes(element) {
  421. const cns = []
  422. for (const cn of element.childNodes) {
  423. cns.push(cn);
  424. }
  425. for (const cn of cns) {
  426. element.removeChild(cn);
  427. }
  428. }
  429.  
  430. // Download and parse a url page into a html document(dom).
  431. // when xhr onload: callback.apply([dom, args])
  432. function getDocument(url, callback, args=[]) {
  433. GM_xmlhttpRequest({
  434. method : 'GET',
  435. url : url,
  436. responseType : 'blob',
  437. onloadstart : function() {
  438. DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
  439. },
  440. onload : function(response) {
  441. const htmlblob = response.response;
  442. parseDocument(htmlblob, callback, args);
  443. }
  444. })
  445. }
  446.  
  447. function parseDocument(htmlblob, callback, args=[]) {
  448. const reader = new FileReader();
  449. reader.onload = function(e) {
  450. const htmlText = reader.result;
  451. const dom = new DOMParser().parseFromString(htmlText, 'text/html');
  452. args = [dom].concat(args);
  453. callback.apply(null, args);
  454. //callback(dom, htmlText);
  455. }
  456. reader.readAsText(htmlblob, document.characterSet);
  457. }
  458.  
  459. // Copy text to clipboard (needs to be called in an user event)
  460. function copyText(text) {
  461. // Create a new textarea for copying
  462. const newInput = document.createElement('textarea');
  463. document.body.appendChild(newInput);
  464. newInput.value = text;
  465. newInput.select();
  466. document.execCommand('copy');
  467. document.body.removeChild(newInput);
  468. }
  469.  
  470. // get '/' splited API array from a url
  471. function getAPI(url=location.href) {
  472. return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
  473. }
  474.  
  475. // get host part from a url(includes '^https://', '/$')
  476. function getHost(url=location.href) {
  477. const match = location.href.match(/https?:\/\/[^\/]+\//);
  478. return match ? match[0] : match;
  479. }
  480.  
  481. function toAbsoluteURL(relativeURL, base=`${location.protocol}//${location.host}/`) {
  482. return new URL(relativeURL, base).href;
  483. }
  484.  
  485. function randint(min, max) {
  486. return Math.floor(Math.random() * (max - min + 1)) + min;
  487. }
  488. })();